File: timestamp.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 -flto -o ./timestamp ./timestamp.c 29 30 If instead you want the app to emit a plain/unstyled timestamp, run 31 32 cc -Wall -s -O3 -flto -D PLAIN -o ./timestamp ./timestamp.c 33 */ 34 35 #include <stdbool.h> 36 #include <stddef.h> 37 #include <stdio.h> 38 #include <stdlib.h> 39 #include <string.h> 40 #include <time.h> 41 42 #ifdef _WIN32 43 #include <fcntl.h> 44 #include <windows.h> 45 #endif 46 47 #ifdef RED_ERRORS 48 #define ERROR_STYLE "\x1b[38;2;204;0;0m" 49 #ifdef __APPLE__ 50 #define ERROR_STYLE "\x1b[31m" 51 #endif 52 #define RESET_STYLE "\x1b[0m" 53 #else 54 #define ERROR_STYLE 55 #define RESET_STYLE 56 #endif 57 58 #define ERROR_LINE(MSG) (ERROR_STYLE MSG RESET_STYLE "\n") 59 60 #ifdef PLAIN 61 #define PLAIN_TIMESTAMP 62 #endif 63 64 #ifndef PLAIN_TIMESTAMP 65 #define STYLE_TIMESTAMP 66 #endif 67 68 // EMIT_CONST emits string constants without their final null byte 69 #define EMIT_CONST(w, x) fwrite(x, 1, sizeof(x) - 1, w) 70 71 #define BAD_ALLOC 2 72 73 const char* info = "" 74 "timestamp [options...]\n" 75 "\n" 76 "Timestamp each line read from stdin with the time it was received.\n" 77 "\n" 78 "Options, all of which can start with either 1 or 2 dashes:\n" 79 "\n" 80 " -h show this help message\n" 81 " -help show this help message\n" 82 ""; 83 84 // span is a region of bytes in memory 85 typedef struct span { 86 // ptr is the starting place of the region 87 unsigned char* ptr; 88 89 // len is how many bytes are in the region 90 size_t len; 91 } span; 92 93 // slice is a growable region of bytes in memory 94 typedef struct slice { 95 // ptr is the starting place of the region 96 unsigned char* ptr; 97 98 // cap is how many bytes the memory region has available 99 size_t cap; 100 } slice; 101 102 // init_slice is the constructor for type slice 103 void init_slice(slice* s, size_t cap) { 104 s->ptr = malloc(cap); 105 s->cap = cap; 106 } 107 108 void timestamp_line(FILE* w, span line) { 109 char buf[32]; 110 time_t now = time(NULL); 111 const struct tm* ymdhms = localtime(&now); 112 size_t len = strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", ymdhms); 113 114 #ifdef STYLE_TIMESTAMP 115 EMIT_CONST(w, "\x1b[48;2;218;218;218m\x1b[38;2;0;95;153m"); 116 fwrite(buf, 1, len, w); 117 EMIT_CONST(w, "\x1b[0m\t"); 118 #else 119 fwrite(buf, 1, len, w); 120 fputc('\t', w); 121 #endif 122 123 fwrite(line.ptr, 1, line.len, w); 124 fputc('\n', w); 125 fflush(w); 126 } 127 128 bool bom_start(span s) { 129 const unsigned char* p = s.ptr; 130 return s.len >= 3 && p[0] == 0xef && p[1] == 0xbb && p[2] == 0xbf; 131 } 132 133 // handle_lines loops over input lines, restyling all digit-runs as more 134 // readable `nice numbers`, fulfilling the app's purpose 135 void handle_lines(FILE* w, slice* line, FILE* src) { 136 span trimmed; 137 138 for (size_t i = 0; !feof(w); i++) { 139 ssize_t len = getline((char**)&line->ptr, &line->cap, src); 140 if (line->ptr == NULL) { 141 fprintf(stderr, "\n"); 142 fprintf(stderr, ERROR_LINE("out of memory")); 143 exit(BAD_ALLOC); 144 } 145 146 if (len < 0) { 147 break; 148 } 149 150 trimmed.ptr = line->ptr; 151 trimmed.len = len; 152 153 // get rid of leading UTF-8 BOM (byte-order mark) if 1st line has it 154 if (i == 0 && bom_start(trimmed)) { 155 trimmed.ptr += 3; 156 trimmed.len -= 3; 157 len = trimmed.len; 158 } 159 160 const unsigned char* p = trimmed.ptr; 161 // get rid of trailing line-feeds and CRLF end-of-line byte-pairs 162 if (len >= 2 && p[len - 2] == '\r' && p[len - 1] == '\n') { 163 trimmed.len -= 2; 164 } else if (len >= 1 && p[len - 1] == '\n') { 165 trimmed.len--; 166 } 167 168 timestamp_line(w, trimmed); 169 } 170 } 171 172 // handle_file handles data from the filename given; returns false only when 173 // the file can't be opened 174 bool handle_file(FILE* w, slice* line, const char* path) { 175 FILE* f = fopen(path, "rb"); 176 if (f == NULL) { 177 fprintf(stderr, ERROR_LINE("can't open file named '%s'"), path); 178 return false; 179 } 180 181 handle_lines(w, line, f); 182 fclose(f); 183 return true; 184 } 185 186 // run returns the number of errors 187 int run(int argc, char** argv, FILE* w, slice* line) { 188 size_t errors = 0; 189 const size_t narg = argc; 190 for (size_t i = 1; i < narg && !feof(w) && line->ptr != NULL; i++) { 191 if (argv[i][0] == '-' && argv[i][1] == 0) { 192 handle_lines(w, line, stdin); 193 continue; 194 } 195 196 if (!handle_file(w, line, argv[i])) { 197 errors++; 198 } 199 } 200 201 // use stdin when not given any filepaths 202 if (argc < 2) { 203 handle_lines(w, line, stdin); 204 } 205 206 return errors; 207 } 208 209 // is_help_option simplifies control-flow for func main 210 bool is_help_option(const char* s) { 211 return (s[0] == '-') && ( 212 strcmp(s, "-h") == 0 || 213 strcmp(s, "-help") == 0 || 214 strcmp(s, "--h") == 0 || 215 strcmp(s, "--help") == 0 216 ); 217 } 218 219 int main(int argc, char** argv) { 220 #ifdef _WIN32 221 setmode(fileno(stdin), O_BINARY); 222 // ensure output lines end in LF instead of CRLF on windows 223 setmode(fileno(stdout), O_BINARY); 224 setmode(fileno(stderr), O_BINARY); 225 #endif 226 227 // handle any of the help options, if given 228 if (argc > 1 && is_help_option(argv[1])) { 229 printf("%s", info); 230 return 0; 231 } 232 233 slice line; 234 init_slice(&line, 32 * 1024); 235 if (line.ptr == NULL) { 236 fprintf(stderr, ERROR_LINE("out of memory")); 237 exit(BAD_ALLOC); 238 } 239 240 const int res = run(argc, argv, stdout, &line) == 0 ? 0 : 1; 241 free(line.ptr); 242 return res; 243 }