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