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 }