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 }