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