File: tcatl.c 1 /* 2 The MIT License (MIT) 3 4 Copyright © 2020-2025 pacman64 5 6 Permission is hereby granted, free of charge, to any person obtaining a copy of 7 this software and associated documentation files (the “Software”), to deal 8 in the Software without restriction, including without limitation the rights to 9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 of the Software, and to permit persons to whom the Software is furnished to do 11 so, subject to the following conditions: 12 13 The above copyright notice and this permission notice shall be included in all 14 copies or substantial portions of the Software. 15 16 THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 SOFTWARE. 23 */ 24 25 /* 26 You can build this command-line app by running 27 28 cc -Wall -s -O2 -o ./tcatl ./tcatl.c 29 */ 30 31 #include <stdbool.h> 32 #include <stdio.h> 33 #include <stdlib.h> 34 #include <string.h> 35 36 #ifdef _WIN32 37 #include <fcntl.h> 38 #include <windows.h> 39 #endif 40 41 // #ifndef RAW_LINES 42 // #define FIX_LINES 43 // #endif 44 45 const char* info = "" 46 "tcatl [filenames...]\n" 47 "\n" 48 "Title and Concatenate lines emits lines from all the named sources given,\n" 49 "preceding each file's contents with its name, using an ANSI reverse style.\n" 50 "\n" 51 "The name `-` stands for the standard input. When no names are given, the\n" 52 "standard input is used by default.\n" 53 ""; 54 55 const char* no_line_memory_msg = "can't get enough memory to read lines"; 56 57 // slice is a growable region of bytes in memory 58 typedef struct slice { 59 // ptr is the starting place of the region 60 unsigned char* ptr; 61 62 // len is how many bytes are currently being used 63 size_t len; 64 65 // cap is how many bytes the memory region has available 66 size_t cap; 67 } slice; 68 69 #ifdef FIX_LINES 70 bool starts_with_bom(const unsigned char* b, const size_t n) { 71 return (n >= 3 && b[0] == 0xef && b[1] == 0xbb && b[2] == 0xbf); 72 } 73 74 // handle_reader skips leading UTF-8 BOMs (byte-order marks), and turns all 75 // CR-LF pairs into single LF bytes 76 bool handle_reader(FILE* w, FILE* r, slice* line) { 77 slice trimmed; 78 79 for (size_t i = 0; !feof(w); i++) { 80 ssize_t len = getline((char**)&line->ptr, &line->cap, r); 81 if (len < 0) { 82 break; 83 } 84 85 if (line->ptr == NULL) { 86 putc('\n', w); 87 fprintf(stderr, "\x1b[31m%s\x1b[0m\n", no_line_memory_msg); 88 return false; 89 } 90 91 line->len = len; 92 trimmed.ptr = line->ptr; 93 trimmed.len = line->len; 94 95 // get rid of leading UTF-8 BOM (byte-order mark) if 1st line has it 96 if (i == 0 && starts_with_bom(trimmed.ptr, trimmed.len)) { 97 trimmed.ptr += 3; 98 trimmed.len -= 3; 99 len = trimmed.len; 100 } 101 102 const unsigned char* p = trimmed.ptr; 103 // get rid of trailing line-feeds and CRLF end-of-line byte-pairs 104 if (len >= 2 && p[len - 2] == '\r' && p[len - 1] == '\n') { 105 trimmed.len -= 2; 106 } else if (len >= 1 && p[len - 1] == '\n') { 107 trimmed.len--; 108 } 109 110 fwrite(trimmed.ptr, trimmed.len, 1, w); 111 putc('\n', w); 112 fflush(w); 113 } 114 115 return true; 116 } 117 #else 118 // handle_reader skips leading UTF-8 BOMs (byte-order marks), and turns all 119 // CR-LF pairs into single LF bytes 120 bool handle_reader(FILE* w, FILE* r, slice* line) { 121 while (!feof(w)) { 122 ssize_t len = getline((char**)&line->ptr, &line->cap, r); 123 if (len < 0) { 124 break; 125 } 126 127 if (line->ptr == NULL) { 128 putc('\n', w); 129 fprintf(stderr, "\x1b[31m%s\x1b[0m\n", no_line_memory_msg); 130 return false; 131 } 132 133 if (len >= 1 && line->ptr[len - 1] == '\n') { 134 len--; 135 } 136 137 fwrite(line->ptr, len, 1, w); 138 putc('\n', w); 139 fflush(w); 140 } 141 142 return true; 143 } 144 #endif 145 146 // handle_file handles data from the filename given; returns false only when 147 // the file can't be opened 148 bool handle_file(FILE* w, const char* fname, slice* line) { 149 fprintf(w, "\x1b[7m%s\x1b[0m\n", fname); 150 fflush(w); 151 152 FILE* f = fopen(fname, "rb"); 153 if (f == NULL) { 154 fprintf(stderr, "\x1b[31mcan't open file named %s\x1b[0m\n", fname); 155 return false; 156 } 157 158 const bool ok = handle_reader(w, f, line); 159 fclose(f); 160 return ok; 161 } 162 163 // run returns the number of errors 164 int run(int argc, char** argv, FILE* w) { 165 size_t dashes = 0; 166 for (int i = 1; i < argc; i++) { 167 if (argv[i][0] == '-' && argv[i][1] == 0) { 168 dashes++; 169 } 170 } 171 172 if (dashes > 1) { 173 const char* msg = "can't use the standard input (dash) more than once"; 174 fprintf(stderr, "\x1b[31m%s\x1b[0m\n", msg); 175 return 1; 176 } 177 178 slice line; 179 line.len = 0; 180 line.cap = 32 * 1024; 181 line.ptr = malloc(line.cap); 182 183 if (line.ptr == NULL) { 184 fprintf(stderr, "\x1b[31m%s\x1b[0m\n", no_line_memory_msg); 185 return 1; 186 } 187 188 size_t errors = 0; 189 for (int i = 1; i < argc && !feof(stdout) && line.ptr != NULL; i++) { 190 if (argv[i][0] == '-' && argv[i][1] == 0) { 191 fputs("\x1b[7m-\x1b[0m\n", w); 192 fflush(w); 193 194 if (!handle_reader(w, stdin, &line)) { 195 errors++; 196 } 197 continue; 198 } 199 200 if (!handle_file(w, argv[i], &line)) { 201 errors++; 202 } 203 } 204 205 // use stdin when not given any filepaths 206 if (argc <= 1) { 207 fputs("\x1b[7m-\x1b[0m\n", w); 208 fflush(w); 209 210 if (!handle_reader(w, stdin, &line)) { 211 errors++; 212 } 213 } 214 215 free(line.ptr); 216 return errors; 217 } 218 219 int main(int argc, char** argv) { 220 #ifdef _WIN32 221 setmode(fileno(stdin), O_BINARY); 222 // ensure output lines end in LF instead of CRLF on windows 223 setmode(fileno(stdout), O_BINARY); 224 setmode(fileno(stderr), O_BINARY); 225 #endif 226 227 if (argc > 1) { 228 if ( 229 strcmp(argv[1], "-h") == 0 || 230 strcmp(argv[1], "-help") == 0 || 231 strcmp(argv[1], "--h") == 0 || 232 strcmp(argv[1], "--help") == 0 233 ) { 234 fprintf(stdout, "%s", info); 235 return 0; 236 } 237 } 238 239 return run(argc, argv, stdout) == 0 ? 0 : 1; 240 }