File: j0.c
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright © 2024 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 j0 [options...] [file...]
  27 
  28 
  29 Json-0 converts/fixes JSON/pseudo-JSON input into minimal JSON output.
  30 Its output is always a single line, which ends with a line-feed.
  31 
  32 Besides minimizing bytes, this tool also adapts almost-JSON input into
  33 valid JSON, since it
  34 
  35     - ignores both rest-of-line and multi-line comments
  36     - ignores extra/trailing commas in arrays and objects
  37     - turns single-quoted strings/keys into double-quoted strings
  38     - double-quotes unquoted object keys
  39     - changes \x 2-hex-digit into \u 4-hex-digit string-escapes
  40 
  41 The only option available can either start with a single or a double-dash
  42 
  43     -h   -help       show this help message
  44 */
  45 
  46 /*
  47 You can build this command-line app by running
  48 
  49 cc -Wall -s -O2 -o ./j0 ./j0.c
  50 */
  51 
  52 #include <ctype.h>
  53 #include <fcntl.h>
  54 #include <stdarg.h>
  55 #include <stdbool.h>
  56 #include <stdint.h>
  57 #include <stdio.h>
  58 #include <stdlib.h>
  59 #include <string.h>
  60 
  61 #ifdef _WIN32
  62 #include <windows.h>
  63 #endif
  64 
  65 // info is the message shown when this app is given any of its help options
  66 const char* info = ""
  67 "j0 [options...] [file...]\n"
  68 "\n"
  69 "\n"
  70 "Json-0 converts/fixes JSON/pseudo-JSON input into minimal JSON output.\n"
  71 "Its output is always a single line, which ends with a line-feed.\n"
  72 "\n"
  73 "Besides minimizing bytes, this tool also adapts almost-JSON input into\n"
  74 "valid JSON, since it\n"
  75 "\n"
  76 "    - ignores both rest-of-line and multi-line comments\n"
  77 "    - ignores extra/trailing commas in arrays and objects\n"
  78 "    - turns single-quoted strings/keys into double-quoted strings\n"
  79 "    - double-quotes unquoted object keys\n"
  80 "    - changes \\x 2-hex-digit into \\u 4-hex-digit string-escapes\n"
  81 "\n"
  82 "All options available can either start with a single or a double-dash\n"
  83 "\n"
  84 "    -h        show this help message\n"
  85 "    -help     show this help message\n"
  86 "    -jsonl    emit JSON Lines, when top-level value is an array\n"
  87 "";
  88 
  89 typedef struct j0_maker {
  90     FILE* in;
  91     unsigned char* ibuf;
  92     size_t ilen; // how many bytes are being used in the input buffer
  93     size_t icap; // the input buffer's capacity
  94     size_t ipos; // the current position in the input buffer
  95 
  96     FILE* out;
  97 
  98     size_t line; // the current line, used to show useful error messages
  99     size_t pos;  // the position in the current line, for error messages
 100 
 101     int current;
 102     int next;
 103 } j0_maker;
 104 
 105 // advance_reader_pos helps func read_byte do its job
 106 void advance_reader_pos(j0_maker* r, unsigned char b) {
 107     r->ipos++;
 108     if (b == '\n') {
 109         r->line++;
 110         r->pos = 1;
 111     } else {
 112         r->pos++;
 113     }
 114 }
 115 
 116 // read_byte does as it says: check its return for the value EOF, before
 117 // using it as the next byte
 118 int read_byte(j0_maker* r) {
 119     if (r->ipos < r->ilen) {
 120         // inside current chunk
 121         const unsigned char b = r->ibuf[r->ipos];
 122         advance_reader_pos(r, b);
 123         return b;
 124     }
 125 
 126     // need to read the next block
 127     r->ipos = 0;
 128     r->ilen = fread(r->ibuf, sizeof(unsigned char), r->icap, r->in);
 129     if (r->ilen > 0) {
 130         const unsigned char b = r->ibuf[r->ipos];
 131         advance_reader_pos(r, b);
 132         return b;
 133     }
 134 
 135     // reached the end of data
 136     return EOF;
 137 }
 138 
 139 // advance is used in most of the code, instead of calling read_byte directly
 140 void advance(j0_maker* r) {
 141     r->current = r->next;
 142     r->next = read_byte(r);
 143 }
 144 
 145 void fail(j0_maker* s, size_t code, const char* fmt, ...);
 146 
 147 void skip_line(j0_maker* r) {
 148     while (true) {
 149         advance(r);
 150         if (r->current == EOF) {
 151             break;
 152         }
 153 
 154         if (r->current == '\n') {
 155             advance(r);
 156             break;
 157         }
 158     }
 159 }
 160 
 161 void skip_multiline_comment(j0_maker* r) {
 162     unsigned char prev = 0;
 163 
 164     while (true) {
 165         advance(r);
 166 
 167         if (r->current == EOF) {
 168             break;
 169         }
 170 
 171         if (prev == '*' && r->current == '/') {
 172             advance(r);
 173             break;
 174         }
 175 
 176         prev = (unsigned char)r->current;
 177     }
 178 }
 179 
 180 void skip_comment(j0_maker* r) {
 181     if (r->current != '/') {
 182         fail(r, 1, "expected a slash to start comments");
 183     }
 184     advance(r);
 185 
 186     if (r->current == '/') {
 187         skip_line(r);
 188         return;
 189     }
 190 
 191     if (r->current == '*') {
 192         skip_multiline_comment(r);
 193         return;
 194     }
 195 
 196     fail(r, 1, "expected `//` or `/*` to start comments");
 197 }
 198 
 199 void seek_token(j0_maker* r) {
 200     while (true) {
 201         if (r->current != EOF && r->current <= ' ') {
 202             advance(r);
 203             continue;
 204         }
 205 
 206         if (r->current == '/') {
 207             skip_comment(r);
 208             continue;
 209         }
 210 
 211         break;
 212     }
 213 }
 214 
 215 bool starts_with_bom(const unsigned char* b, const size_t n) {
 216     return (n >= 3 && b[0] == 0xef && b[1] == 0xbb && b[2] == 0xbf);
 217 }
 218 
 219 void restart_state(j0_maker* s, FILE* w, FILE* r) {
 220     s->in = r;
 221     s->ilen = 0;
 222     s->ipos = 0;
 223 
 224     s->out = w;
 225 
 226     s->line = 1;
 227     s->pos = 1;
 228 
 229     s->current = EOF;
 230     s->next = EOF;
 231 
 232     s->current = read_byte(s);
 233     if (s->current == EOF) {
 234         return;
 235     }
 236     s->next = read_byte(s);
 237 
 238     // skip leading UTF-8 BOM (byte-order mark), if present
 239     if (starts_with_bom(s->ibuf, s->ilen)) {
 240         // a UTF-8 BOM has 3 bytes
 241         for (size_t i = 0; i < 3 && s->current != EOF; i++) {
 242             advance(s);
 243         }
 244     }
 245 }
 246 
 247 // write_bytes does as it says, minimizing the number of calls to fwrite
 248 void write_bytes(j0_maker* w, const unsigned char* src, size_t len) {
 249     if (len > 0 && fwrite(src, len, 1, w->out) < 1) {
 250         if (feof(w->out)) {
 251             exit(0);
 252         }
 253 
 254         fail(w, 1, "failed to write more output");
 255     }
 256 }
 257 
 258 inline void write_byte(j0_maker* w, unsigned char b) {
 259     putc(b, w->out);
 260 }
 261 
 262 // debug is available to diagnose any bug found
 263 void debug(j0_maker* s, const char* fmt, ...) {
 264     va_list args;
 265     va_start(args, fmt);
 266 
 267     if (s->in != stdin) {
 268         fclose(s->in);
 269     }
 270 
 271     write_byte(s, '\n');
 272 
 273     const unsigned long line = s->line;
 274     const unsigned long pos = s->pos;
 275     fprintf(stderr, "\x1b[46m\x1b[37mline %lu, pos %lu: ", line, pos);
 276     fprintf(stderr, fmt, args);
 277     fprintf(stderr, "\x1b[0m\n");
 278 
 279     va_end(args);
 280 
 281     exit(10);
 282 }
 283 
 284 // fail quits this app with the printf-style formatted error message given
 285 void fail(j0_maker* s, size_t code, const char* fmt, ...) {
 286     va_list args;
 287     va_start(args, fmt);
 288 
 289     if (s->in != stdin) {
 290         fclose(s->in);
 291     }
 292 
 293     write_byte(s, '\n');
 294 
 295     const unsigned long line = s->line;
 296     const unsigned long pos = s->pos;
 297     fprintf(stderr, "\x1b[31mline %lu, pos %lu: ", line, pos);
 298     fprintf(stderr, fmt, args);
 299     fprintf(stderr, "\x1b[0m\n");
 300 
 301     va_end(args);
 302 
 303     exit(code);
 304 }
 305 
 306 bool demand_keyword(j0_maker* s, char* rest) {
 307     for (; rest[0] != 0; rest++) {
 308         if (s->current == EOF || s->current != rest[0]) {
 309             return false;
 310         }
 311         advance(s);
 312     }
 313 
 314     return rest[0] == 0;
 315 }
 316 
 317 void handle_null(j0_maker* s) {
 318     if (!demand_keyword(s, "null")) {
 319         fail(s, 1, "expected `null` keyword");
 320     }
 321     write_bytes(s, (unsigned char*)"null", 4);
 322 }
 323 
 324 void handle_true(j0_maker* s) {
 325     if (!demand_keyword(s, "true")) {
 326         fail(s, 1, "expected `true` keyword");
 327     }
 328     write_bytes(s, (unsigned char*)"true", 4);
 329 }
 330 
 331 void handle_false(j0_maker* s) {
 332     if (!demand_keyword(s, "false")) {
 333         fail(s, 1, "expected `false` keyword");
 334     }
 335     write_bytes(s, (unsigned char*)"false", 5);
 336 }
 337 
 338 void handle_capital_none(j0_maker* s) {
 339     if (!demand_keyword(s, "None")) {
 340         fail(s, 1, "expected `None` keyword");
 341     }
 342     write_bytes(s, (unsigned char*)"null", 4);
 343 }
 344 
 345 void handle_capital_true(j0_maker* s) {
 346     if (!demand_keyword(s, "True")) {
 347         fail(s, 1, "expected `True` keyword");
 348     }
 349     write_bytes(s, (unsigned char*)"true", 4);
 350 }
 351 
 352 void handle_capital_false(j0_maker* s) {
 353     if (!demand_keyword(s, "False")) {
 354         fail(s, 1, "expected `False` keyword");
 355     }
 356     write_bytes(s, (unsigned char*)"false", 5);
 357 }
 358 
 359 void handle_digits(j0_maker* s) {
 360     if (!isdigit(s->current)) {
 361         fail(s, 1, "expected/missing digits");
 362     }
 363 
 364     while (isdigit(s->current)) {
 365         write_byte(s, s->current);
 366         advance(s);
 367     }
 368 }
 369 
 370 void handle_number(j0_maker* s) {
 371     handle_digits(s);
 372 
 373     if (s->current == '.') {
 374         write_byte(s, '.');
 375         advance(s);
 376 
 377         if (isdigit(s->current)) {
 378             handle_digits(s);
 379         } else {
 380             write_byte(s, '0');
 381         }
 382         return;
 383     }
 384 
 385     if (s->current == 'e' || s->current == 'E') {
 386         write_byte(s, s->current);
 387         advance(s);
 388 
 389         if (s->current == '+') {
 390             advance(s);
 391         } else if (s->current == '-') {
 392             write_byte(s, '-');
 393             advance(s);
 394         }
 395 
 396         handle_digits(s);
 397     }
 398 }
 399 
 400 void handle_dot(j0_maker* s) {
 401     write_byte(s, '0');
 402     write_byte(s, '.');
 403     advance(s);
 404 
 405     if (!isdigit(s->current)) {
 406         fail(s, 1, "expected/missing digits after decimal dot");
 407     }
 408     handle_digits(s);
 409 }
 410 
 411 void handle_plus_number(j0_maker* s) {
 412     advance(s);
 413 
 414     if (s->current == '.') {
 415         handle_dot(s);
 416         return;
 417     }
 418     handle_number(s);
 419 }
 420 
 421 void handle_minus_number(j0_maker* s) {
 422     write_byte(s, '-');
 423     advance(s);
 424 
 425     if (s->current == '.') {
 426         handle_dot(s);
 427         return;
 428     }
 429     handle_number(s);
 430 }
 431 
 432 void handle_string_escape(j0_maker* s, int c) {
 433     switch (c) {
 434         case '"':
 435         case '\\':
 436         case 'b':
 437         case 'f':
 438         case 'n':
 439         case 'r':
 440         case 't':
 441             write_byte(s, '\\');
 442             write_byte(s, c);
 443             break;
 444 
 445         case 'u':
 446             write_byte(s, '\\');
 447             write_byte(s, 'u');
 448             for (size_t i = 0; i < 4; i++) {
 449                 advance(s);
 450                 if (s->current == EOF) {
 451                     fail(s, 1, "end of input before end of string");
 452                 }
 453                 if (isdigit(s->current) || isalpha(s->current)) {
 454                     // write_byte(s, toupper(c));
 455                     write_byte(s, c);
 456                     continue;
 457                 }
 458                 fail(s, 1, "invalid hexadecimal digit in string");
 459             }
 460             break;
 461 
 462         case 'x':
 463             write_byte(s, '\\');
 464             write_byte(s, 'u');
 465             write_byte(s, '0');
 466             write_byte(s, '0');
 467             for (size_t i = 0; i < 2; i++) {
 468                 advance(s);
 469                 if (s->current == EOF) {
 470                     fail(s, 1, "end of input before end of string");
 471                 }
 472                 if (isdigit(s->current) || isalpha(s->current)) {
 473                     // write_byte(s, toupper(c));
 474                     write_byte(s, c);
 475                     continue;
 476                 }
 477                 fail(s, 1, "invalid hexadecimal digit in string");
 478             }
 479             break;
 480 
 481         case '\'':
 482             write_byte(s, '\'');
 483             break;
 484 
 485         default:
 486             write_byte(s, s->current);
 487             break;
 488     }
 489 }
 490 
 491 void handle_string(j0_maker* s) {
 492     const unsigned char quote = s->current;
 493     bool escaped = false;
 494 
 495     write_byte(s, '"');
 496 
 497     while (true) {
 498         advance(s);
 499 
 500         int c = s->current;
 501         if (c == EOF) {
 502             fail(s, 1, "input ended before string was close-quoted");
 503         }
 504 
 505         if (escaped) {
 506             handle_string_escape(s, c);
 507             escaped = false;
 508             continue;
 509         }
 510 
 511         switch (c) {
 512             case '\\':
 513                 escaped = true;
 514                 break;
 515 
 516             default:
 517                 if (c == quote) {
 518                     write_byte(s, '"');
 519                     advance(s);
 520                     return;
 521                 }
 522 
 523                 write_byte(s, c);
 524                 break;
 525         }
 526     }
 527 }
 528 
 529 void handle_token(j0_maker* s);
 530 
 531 void handle_array(j0_maker* s) {
 532     size_t items_before = 0;
 533     write_byte(s, '[');
 534     advance(s);
 535 
 536     while (true) {
 537         seek_token(s);
 538         if (s->current == EOF) {
 539             fail(s, 1, "unclosed array");
 540         }
 541 
 542         if (s->current == ',') {
 543             advance(s);
 544             continue;
 545         }
 546 
 547         if (s->current == ']') {
 548             write_byte(s, ']');
 549             advance(s);
 550             return;
 551         }
 552 
 553         if (items_before > 0) {
 554             write_byte(s, ',');
 555         }
 556         handle_token(s);
 557         items_before++;
 558     }
 559 }
 560 
 561 // handle_array_jsonl is a slight variation of func handle_array: this one is
 562 // used to handle top-level arrays when running in JSON Lines mode, to emit
 563 // line-feeds after each item, instead of commas between them
 564 void handle_array_jsonl(j0_maker* s) {
 565     size_t items_before = 0;
 566     advance(s);
 567 
 568     while (true) {
 569         seek_token(s);
 570         if (s->current == EOF) {
 571             fail(s, 1, "unclosed array");
 572         }
 573 
 574         if (s->current == ',') {
 575             advance(s);
 576             continue;
 577         }
 578 
 579         if (items_before > 0) {
 580             write_byte(s, '\n');
 581         }
 582 
 583         if (s->current == ']') {
 584             advance(s);
 585             return;
 586         }
 587 
 588         handle_token(s);
 589         items_before++;
 590     }
 591 }
 592 
 593 void handle_unquoted_key(j0_maker* s) {
 594     write_byte(s, '"');
 595 
 596     while (true) {
 597         int c = s->current;
 598         if (c == EOF) {
 599             fail(s, 1, "input ended with an object key");
 600         }
 601 
 602         write_byte(s, c);
 603         advance(s);
 604 
 605         c = s->current;
 606         if (!isalpha(c) && !isdigit(c) && c != '_') {
 607             break;
 608         }
 609     }
 610 
 611     write_byte(s, '"');
 612 }
 613 
 614 void handle_object(j0_maker* s) {
 615     size_t items_before = 0;
 616     write_byte(s, '{');
 617     advance(s);
 618 
 619     while (true) {
 620         seek_token(s);
 621         if (s->current == EOF) {
 622             fail(s, 1, "unclosed object");
 623         }
 624 
 625         if (s->current == ',') {
 626             advance(s);
 627             continue;
 628         }
 629 
 630         if (s->current == '}') {
 631             write_byte(s, '}');
 632             advance(s);
 633             return;
 634         }
 635 
 636         if (s->current == '"' || s->current == '\'') {
 637             if (items_before > 0) {
 638                 write_byte(s, ',');
 639             }
 640             handle_string(s);
 641             items_before++;
 642         } else if (isalpha(s->current) || s->current == '_') {
 643             if (items_before > 0) {
 644                 write_byte(s, ',');
 645             }
 646             handle_unquoted_key(s);
 647             items_before++;
 648         } else {
 649             fail(s, 1, "only strings or identifiers can be object keys");
 650         }
 651 
 652         seek_token(s);
 653         if (s->current == EOF) {
 654             fail(s, 1, "input ended after object-key and before value");
 655         }
 656 
 657         if (s->current != ':') {
 658             fail(s, 1, "a `:` must follow all object keys");
 659         }
 660 
 661         write_byte(s, ':');
 662         advance(s);
 663 
 664         seek_token(s);
 665         if (s->current == EOF) {
 666             fail(s, 1, "input ended after a `:` following an object-key");
 667         }
 668 
 669         handle_token(s);
 670     }
 671 }
 672 
 673 // dispatch ties leading bytes/chars in tokens to the funcs which handle them
 674 void (*dispatch[256])() = {
 675     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 676     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 677     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 678     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 679     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 680     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 681     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 682     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 683     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 684     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 685     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 686     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 687     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 688     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 689     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 690     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 691     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 692     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 693     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 694     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 695     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 696     NULL, NULL, NULL, NULL,
 697 };
 698 
 699 void handle_token(j0_maker* s) {
 700     void (*fn)(j0_maker*) = NULL;
 701 
 702     // seek_token(s);
 703     // if (s->current == EOF) {
 704     //     fail(s, 1, "expected a token");
 705     // }
 706 
 707     fn = dispatch[s->current];
 708     if (fn != NULL) {
 709         fn(s);
 710     } else {
 711         unsigned char c = (unsigned char)s->current;
 712         fprintf(stderr, "%c\n", c);
 713         fail(s, 1, "invalid token");
 714     }
 715 }
 716 
 717 void handle_array_jsonl(j0_maker* s);
 718 
 719 void handle_input(FILE* src, bool jsonl) {
 720     unsigned char ibuf[32 * 1024];
 721 
 722     j0_maker state;
 723     j0_maker* s = &state;
 724     s->ibuf = ibuf;
 725     s->icap = sizeof(ibuf);
 726     restart_state(s, stdout, src);
 727 
 728     // ignore leading whitespace/comment bytes, if present
 729     seek_token(s);
 730 
 731     if (s->current == EOF) {
 732         fail(s, 1, "empty input isn't valid JSON");
 733     }
 734 
 735     if (jsonl && s->current == '[') {
 736         handle_array_jsonl(s);
 737     } else {
 738         handle_token(s);
 739         write_byte(s, '\n');
 740     }
 741 
 742     // ignore trailing whitespace/comment bytes, if present
 743     seek_token(s);
 744 
 745     // ignore trailing semicolon, if present
 746     if (s->current == ';') {
 747         advance(s);
 748         // ignore trailing whitespace/comment bytes, if present
 749         seek_token(s);
 750     }
 751 
 752     if (!feof(src)) {
 753         fail(s, 1, "unexpected trailing JSON data");
 754     }
 755 }
 756 
 757 bool is_help_option(const char* s) {
 758     return (s[0] == '-' && s[1] != 0) && (
 759         strcmp(s, "-h") == 0 || strcmp(s, "--h") == 0 ||
 760         strcmp(s, "-help") == 0 || strcmp(s, "--help") == 0
 761     );
 762 }
 763 
 764 bool is_jsonl_option(const char* s) {
 765     return (s[0] == '-' && s[1] != 0) && (
 766         strcmp(s, "-jsonl") == 0 || strcmp(s, "--jsonl") == 0
 767     );
 768 }
 769 
 770 // run returns the error code
 771 int run(int argc, char** argv) {
 772     bool jsonl = false;
 773     if (argc > 1 && is_jsonl_option(argv[1])) {
 774         jsonl = true;
 775         argc--;
 776         argv++;
 777     }
 778 
 779     if (argc > 2) {
 780         const char* msg = "can't use more than 1 named input";
 781         fprintf(stderr, "\x1b[31m%s\x1b[0m\n", msg);
 782         return 1;
 783     }
 784 
 785     // use stdin when not given a filepath, or is `-`
 786     if (argc < 2 || argv[1][0] == 0 || strcmp(argv[1], "-") == 0) {
 787         handle_input(stdin, jsonl);
 788         return 0;
 789     }
 790 
 791     const char* path = argv[1];
 792     FILE* f = fopen(path, "rb");
 793     if (f == NULL) {
 794         fprintf(stderr, "\x1b[31mcan't open file named %s\x1b[0m\n", path);
 795         return 1;
 796     }
 797 
 798     handle_input(f, jsonl);
 799     fclose(f);
 800 
 801     return 0;
 802 }
 803 
 804 int main(int argc, char** argv) {
 805 #ifdef _WIN32
 806     setmode(fileno(stdin), O_BINARY);
 807     // ensure output lines end in LF instead of CRLF on windows
 808     setmode(fileno(stdout), O_BINARY);
 809     setmode(fileno(stderr), O_BINARY);
 810 #endif
 811 
 812     if (argc > 1 && is_help_option(argv[1])) {
 813         puts(info);
 814         return 0;
 815     }
 816 
 817     dispatch['0'] = handle_number;
 818     dispatch['1'] = handle_number;
 819     dispatch['2'] = handle_number;
 820     dispatch['3'] = handle_number;
 821     dispatch['4'] = handle_number;
 822     dispatch['5'] = handle_number;
 823     dispatch['6'] = handle_number;
 824     dispatch['7'] = handle_number;
 825     dispatch['8'] = handle_number;
 826     dispatch['9'] = handle_number;
 827     dispatch['n'] = handle_null;
 828     dispatch['t'] = handle_true;
 829     dispatch['f'] = handle_false;
 830     dispatch['N'] = handle_capital_none;
 831     dispatch['T'] = handle_capital_true;
 832     dispatch['F'] = handle_capital_false;
 833     dispatch['.'] = handle_dot;
 834     dispatch['+'] = handle_plus_number;
 835     dispatch['-'] = handle_minus_number;
 836     dispatch['"'] = handle_string;
 837     dispatch['\''] = handle_string;
 838     dispatch['['] = handle_array;
 839     dispatch['{'] = handle_object;
 840 
 841     return run(argc, argv) == 0 ? 0 : 1;
 842 }