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 <fcntl.h>
  32 #include <stdbool.h>
  33 #include <stdio.h>
  34 #include <stdlib.h>
  35 #include <string.h>
  36 
  37 #ifdef _WIN32
  38 #include <windows.h>
  39 #endif
  40 
  41 const char* info = ""
  42 "tcatl [filenames...]\n"
  43 "\n"
  44 "Title and Concatenate lines emits lines from all the named sources given,\n"
  45 "preceding each file's contents with its name, using an ANSI reverse style.\n"
  46 "\n"
  47 "The name `-` stands for the standard input. When no names are given, the\n"
  48 "standard input is used by default.\n"
  49 "";
  50 
  51 const char* no_line_memory_msg = "can't get enough memory to read lines";
  52 
  53 // slice is a growable region of bytes in memory
  54 typedef struct slice {
  55     // ptr is the starting place of the region
  56     unsigned char* ptr;
  57 
  58     // len is how many bytes are currently being used
  59     size_t len;
  60 
  61     // cap is how many bytes the memory region has available
  62     size_t cap;
  63 } slice;
  64 
  65 bool starts_with_bom(const unsigned char* b, const size_t n) {
  66     return (n >= 3 && b[0] == 0xef && b[1] == 0xbb && b[2] == 0xbf);
  67 }
  68 
  69 // handle_reader skips leading UTF-8 BOMs (byte-order marks), and turns all
  70 // CR-LF pairs into single LF bytes
  71 bool handle_reader(FILE* w, FILE* r, slice* line) {
  72     slice trimmed;
  73 
  74     for (size_t i = 0; !feof(w); i++) {
  75         int len = getline((char**)&line->ptr, &line->cap, r);
  76         if (len < 0) {
  77             break;
  78         }
  79 
  80         if (line->ptr == NULL) {
  81             putc('\n', w);
  82             fflush(w);
  83 
  84             fprintf(stderr, "\x1b[31m%s\x1b[0m\n", no_line_memory_msg);
  85             exit(1);
  86         }
  87 
  88         line->len = len;
  89         trimmed.ptr = line->ptr;
  90         trimmed.len = line->len;
  91 
  92         // get rid of leading UTF-8 BOM (byte-order mark) if 1st line has it
  93         if (i == 0 && starts_with_bom(trimmed.ptr, trimmed.len)) {
  94             trimmed.ptr += 3;
  95             trimmed.len -= 3;
  96             len = trimmed.len;
  97         }
  98 
  99         const unsigned char* p = trimmed.ptr;
 100         // get rid of trailing line-feeds and CRLF end-of-line byte-pairs
 101         if (len >= 2 && p[len - 2] == '\r' && p[len - 1] == '\n') {
 102             trimmed.len -= 2;
 103         } else if (len >= 1 && p[len - 1] == '\n') {
 104             trimmed.len--;
 105         }
 106 
 107         fwrite(trimmed.ptr, trimmed.len, 1, w);
 108         putc('\n', w);
 109         fflush(w);
 110     }
 111 
 112     return true;
 113 }
 114 
 115 // handle_file handles data from the filename given; returns false only when
 116 // the file can't be opened
 117 bool handle_file(FILE* w, char* fname, slice* line) {
 118     fprintf(w, "\x1b[7m%s\x1b[0m\n", fname);
 119     fflush(w);
 120 
 121     FILE* f = fopen(fname, "rb");
 122     if (f == NULL) {
 123         fprintf(stderr, "\x1b[31mcan't open file named %s\x1b[0m\n", fname);
 124         return false;
 125     }
 126 
 127     const bool ok = handle_reader(w, f, line);
 128     fclose(f);
 129     return ok;
 130 }
 131 
 132 // run returns the number of errors
 133 int run(int argc, char** argv, FILE* w) {
 134     size_t dashes = 0;
 135     for (int i = 1; i < argc; i++) {
 136         if (argv[i][0] == '-' && argv[i][1] == 0) {
 137             dashes++;
 138         }
 139     }
 140 
 141     if (dashes > 1) {
 142         const char* msg = "can't use the standard input (dash) more than once";
 143         fprintf(stderr, "\x1b[31m%s\x1b[0m\n", msg);
 144         return 1;
 145     }
 146 
 147     slice line;
 148     line.len = 0;
 149     line.cap = 32 * 1024;
 150     line.ptr = malloc(line.cap);
 151 
 152     if (line.ptr == NULL) {
 153         fprintf(stderr, "\x1b[31m%s\x1b[0m\n", no_line_memory_msg);
 154         return 1;
 155     }
 156 
 157     // use stdin when not given any filepaths
 158     if (argc <= 1) {
 159         fputs("\x1b[7m-\x1b[0m\n", w);
 160         fflush(w);
 161 
 162         handle_reader(w, stdin, &line);
 163         free(line.ptr);
 164         return 0;
 165     }
 166 
 167     size_t errors = 0;
 168     for (int i = 1; i < argc && !feof(stdout); i++) {
 169         if (argv[i][0] == '-' && argv[i][1] == 0) {
 170             fputs("\x1b[7m-\x1b[0m\n", w);
 171             fflush(w);
 172 
 173             if (!handle_reader(w, stdin, &line)) {
 174                 errors++;
 175             }
 176             continue;
 177         }
 178 
 179         if (!handle_file(w, argv[i], &line)) {
 180             errors++;
 181         }
 182     }
 183 
 184     free(line.ptr);
 185     return errors;
 186 }
 187 
 188 int main(int argc, char** argv) {
 189 #ifdef _WIN32
 190     setmode(fileno(stdin), O_BINARY);
 191     // ensure output lines end in LF instead of CRLF on windows
 192     setmode(fileno(stdout), O_BINARY);
 193     setmode(fileno(stderr), O_BINARY);
 194 #endif
 195 
 196     if (argc > 1) {
 197         if (
 198             strcmp(argv[1], "-h") == 0 ||
 199             strcmp(argv[1], "-help") == 0 ||
 200             strcmp(argv[1], "--h") == 0 ||
 201             strcmp(argv[1], "--help") == 0
 202         ) {
 203             fprintf(stdout, "%s", info);
 204             return 0;
 205         }
 206     }
 207 
 208     return run(argc, argv, stdout) == 0 ? 0 : 1;
 209 }