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 }