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