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