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