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