File: catl.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 ./catl ./catl.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 "catl [filenames...]\n"
  63 "\n"
  64 "Concatenate lines from all the named sources given. The name `-` stands for\n"
  65 "the standard input. When no names are given, the standard input is used by\n"
  66 "default.\n"
  67 "";
  68 
  69 // slice is a growable region of bytes in memory
  70 typedef struct slice {
  71     // ptr is the starting place of the region
  72     unsigned char* ptr;
  73 
  74     // cap is how many bytes the memory region has available
  75     size_t cap;
  76 } slice;
  77 
  78 void handle_reader_faster(FILE* w, FILE* r) {
  79     unsigned char buf[IBUF_SIZE];
  80     unsigned char last = '\n';
  81 
  82     while (!feof(w)) {
  83         size_t len = fread(buf, sizeof(buf[0]), sizeof(buf), r);
  84         if (len < 1) {
  85             break;
  86         }
  87 
  88         fwrite(buf, 1, len, w);
  89         last = buf[len - 1];
  90     }
  91 
  92     if (last != '\n') {
  93         fputc('\n', w);
  94     }
  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 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, len, 1, w);
 118         const bool has_lf = len >= 1 && line->ptr[len - 1] == '\n';
 119         if (!has_lf) {
 120             fputc('\n', w);
 121         }
 122         fflush(w);
 123     }
 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 live_lines) {
 129     FILE* f = fopen(path, "rb");
 130     if (f == NULL) {
 131         fprintf(stderr, ERROR_LINE("can't open file named '%s'"), path);
 132         return false;
 133     }
 134 
 135     handle_reader(w, f, line, live_lines);
 136     fclose(f);
 137     return true;
 138 }
 139 
 140 // run returns the number of errors
 141 int run(char** args, size_t nargs, FILE* w, bool live_lines) {
 142     size_t dashes = 0;
 143     for (size_t i = 0; i < nargs; i++) {
 144         if (args[i][0] == '-' && args[i][1] == 0) {
 145             dashes++;
 146         }
 147     }
 148 
 149     if (dashes > 1) {
 150         const char* m = "can't use the standard input (dash) more than once";
 151         fprintf(stderr, ERROR_LINE("%s"), m);
 152         return 1;
 153     }
 154 
 155     slice line;
 156     line.ptr = NULL;
 157     line.cap = 0;
 158 
 159     if (live_lines) {
 160         line.cap = 32 * 1024;
 161         line.ptr = malloc(line.cap);
 162         if (line.ptr == NULL) {
 163             fprintf(stderr, ERROR_LINE("out of memory"));
 164             exit(BAD_ALLOC);
 165         }
 166     }
 167 
 168     size_t errors = 0;
 169     for (size_t i = 0; i < nargs && !feof(w); i++) {
 170         if (args[i][0] == '-' && args[i][1] == 0) {
 171             handle_reader(w, stdin, &line, live_lines);
 172             continue;
 173         }
 174 
 175         if (!handle_file(w, args[i], &line, live_lines)) {
 176             errors++;
 177         }
 178     }
 179 
 180     // use stdin when not given any filepaths
 181     if (nargs < 1) {
 182         handle_reader(w, stdin, &line, live_lines);
 183     }
 184 
 185     free(line.ptr);
 186     return errors;
 187 }
 188 
 189 int main(int argc, char** argv) {
 190 #ifdef _WIN32
 191     setmode(fileno(stdin), O_BINARY);
 192     // ensure output lines end in LF instead of CRLF on windows
 193     setmode(fileno(stdout), O_BINARY);
 194     setmode(fileno(stderr), O_BINARY);
 195 #endif
 196 
 197     if (argc > 1) {
 198         if (
 199             strcmp(argv[1], "-h") == 0 ||
 200             strcmp(argv[1], "-help") == 0 ||
 201             strcmp(argv[1], "--h") == 0 ||
 202             strcmp(argv[1], "--help") == 0
 203         ) {
 204             fprintf(stdout, "%s", info);
 205             return 0;
 206         }
 207     }
 208 
 209     size_t nargs = argc - 1;
 210     char** args = argv + 1;
 211     if (nargs > 0 && strcmp(args[0], "--") == 0) {
 212         nargs--;
 213         args++;
 214     }
 215 
 216     const bool live_lines = lseek(fileno(stdout), 0, SEEK_CUR) != 0;
 217     if (!live_lines) {
 218         setvbuf(stdout, NULL, _IOFBF, 0);
 219     }
 220     return run(args, nargs, stdout, live_lines) == 0 ? 0 : 1;
 221 }