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