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 }