File: ngron.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 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 To compile a smaller-sized command-line app, you can use the `go` command as
  27 follows:
  28 
  29 go build -ldflags "-s -w" -trimpath ngron.go
  30 */
  31 
  32 package main
  33 
  34 import (
  35     "bufio"
  36     "encoding/json"
  37     "errors"
  38     "io"
  39     "os"
  40     "strconv"
  41 )
  42 
  43 const info = `
  44 ngron [options...] [filepath/URI...]
  45 
  46 
  47 Nice GRON converts JSON data into 'grep'-friendly lines, similar to what
  48 tool 'gron' (GRep jsON; https://github.com/tomnomnom/gron) does.
  49 
  50 This tool uses nicer ANSI styles than the original, hence its name, but
  51 can't convert its output back into JSON, unlike the latter.
  52 
  53 Unlike the original 'gron', there's no sort-mode. When not given a named
  54 source (filepath/URI) to read from, data are read from standard input.
  55 
  56 Options, where leading double-dashes are also allowed:
  57 
  58     -h         show this help message
  59     -help      show this help message
  60 
  61     -m         monochrome (default), enables unstyled output-mode
  62     -c         color, enables ANSI-styled output-mode
  63     -color     enables ANSI-styled output-mode
  64 `
  65 
  66 type emitConfig struct {
  67     path    func(w *bufio.Writer, path []any) error
  68     null    func(w *bufio.Writer) error
  69     boolean func(w *bufio.Writer, b bool) error
  70     number  func(w *bufio.Writer, n json.Number) error
  71     key     func(w *bufio.Writer, k string) error
  72     text    func(w *bufio.Writer, s string) error
  73 
  74     arrayDecl  string
  75     objectDecl string
  76 }
  77 
  78 var monochrome = emitConfig{
  79     path:    monoPath,
  80     null:    monoNull,
  81     boolean: monoBool,
  82     number:  monoNumber,
  83     key:     monoString,
  84     text:    monoString,
  85 
  86     arrayDecl:  `[]`,
  87     objectDecl: `{}`,
  88 }
  89 
  90 var styled = emitConfig{
  91     path:    styledPath,
  92     null:    styledNull,
  93     boolean: styledBool,
  94     number:  styledNumber,
  95     key:     styledString,
  96     text:    styledString,
  97 
  98     arrayDecl:  "\x1b[38;2;168;168;168m[]\x1b[0m",
  99     objectDecl: "\x1b[38;2;168;168;168m{}\x1b[0m",
 100 }
 101 
 102 var config = monochrome
 103 
 104 func main() {
 105     args := os.Args[1:]
 106 
 107     for len(args) > 0 {
 108         switch args[0] {
 109         case `-c`, `--c`, `-color`, `--color`:
 110             config = styled
 111             args = args[1:]
 112             continue
 113 
 114         case `-h`, `--h`, `-help`, `--help`:
 115             os.Stdout.WriteString(info[1:])
 116             return
 117 
 118         case `-m`, `--m`:
 119             config = monochrome
 120             args = args[1:]
 121             continue
 122         }
 123 
 124         break
 125     }
 126 
 127     if len(args) > 0 && args[0] == `--` {
 128         args = args[1:]
 129     }
 130 
 131     if len(args) > 2 {
 132         os.Stderr.WriteString("multiple inputs not allowed\n")
 133         os.Exit(1)
 134     }
 135 
 136     // figure out whether input should come from a named file or from stdin
 137     path := `-`
 138     if len(args) > 0 {
 139         path = args[0]
 140     }
 141 
 142     err := handleInput(os.Stdout, path)
 143     if err != nil && err != io.EOF {
 144         os.Stderr.WriteString(err.Error())
 145         os.Stderr.WriteString("\n")
 146         os.Exit(1)
 147     }
 148 }
 149 
 150 type handlerFunc func(*bufio.Writer, *json.Decoder, json.Token, []any) error
 151 
 152 // handleInput simplifies control-flow for func main
 153 func handleInput(w io.Writer, path string) error {
 154     if path == `-` {
 155         bw := bufio.NewWriter(w)
 156         defer bw.Flush()
 157         return run(bw, os.Stdin)
 158     }
 159 
 160     f, err := os.Open(path)
 161     if err != nil {
 162         // on windows, file-not-found error messages may mention `CreateFile`,
 163         // even when trying to open files in read-only mode
 164         return errors.New(`can't open file named ` + path)
 165     }
 166     defer f.Close()
 167 
 168     bw := bufio.NewWriter(w)
 169     defer bw.Flush()
 170     return run(bw, f)
 171 }
 172 
 173 // escapedStringBytes helps func handleString treat all string bytes quickly
 174 // and correctly, using their officially-supported JSON escape sequences
 175 //
 176 // https://www.rfc-editor.org/rfc/rfc8259#section-7
 177 var escapedStringBytes = [256][]byte{
 178     {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'},
 179     {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'},
 180     {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'},
 181     {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'},
 182     {'\\', 'b'}, {'\\', 't'},
 183     {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'},
 184     {'\\', 'f'}, {'\\', 'r'},
 185     {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'},
 186     {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'},
 187     {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'},
 188     {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'},
 189     {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'},
 190     {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'},
 191     {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'},
 192     {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'},
 193     {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'},
 194     {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39},
 195     {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47},
 196     {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55},
 197     {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63},
 198     {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71},
 199     {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79},
 200     {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87},
 201     {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95},
 202     {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103},
 203     {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111},
 204     {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119},
 205     {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127},
 206     {128}, {129}, {130}, {131}, {132}, {133}, {134}, {135},
 207     {136}, {137}, {138}, {139}, {140}, {141}, {142}, {143},
 208     {144}, {145}, {146}, {147}, {148}, {149}, {150}, {151},
 209     {152}, {153}, {154}, {155}, {156}, {157}, {158}, {159},
 210     {160}, {161}, {162}, {163}, {164}, {165}, {166}, {167},
 211     {168}, {169}, {170}, {171}, {172}, {173}, {174}, {175},
 212     {176}, {177}, {178}, {179}, {180}, {181}, {182}, {183},
 213     {184}, {185}, {186}, {187}, {188}, {189}, {190}, {191},
 214     {192}, {193}, {194}, {195}, {196}, {197}, {198}, {199},
 215     {200}, {201}, {202}, {203}, {204}, {205}, {206}, {207},
 216     {208}, {209}, {210}, {211}, {212}, {213}, {214}, {215},
 217     {216}, {217}, {218}, {219}, {220}, {221}, {222}, {223},
 218     {224}, {225}, {226}, {227}, {228}, {229}, {230}, {231},
 219     {232}, {233}, {234}, {235}, {236}, {237}, {238}, {239},
 220     {240}, {241}, {242}, {243}, {244}, {245}, {246}, {247},
 221     {248}, {249}, {250}, {251}, {252}, {253}, {254}, {255},
 222 }
 223 
 224 // run does it all, given a reader and a writer
 225 func run(w *bufio.Writer, r io.Reader) error {
 226     dec := json.NewDecoder(r)
 227     // avoid parsing numbers, so unusually-long numbers are kept verbatim,
 228     // even if JSON parsers aren't required to guarantee such input-fidelity
 229     // for numbers
 230     dec.UseNumber()
 231 
 232     t, err := dec.Token()
 233     if err == io.EOF {
 234         return errors.New(`input has no JSON values`)
 235     }
 236 
 237     if err = handleToken(w, dec, t, make([]any, 0, 50)); err != nil {
 238         return err
 239     }
 240 
 241     _, err = dec.Token()
 242     if err == io.EOF {
 243         // input is over, so it's a success
 244         return nil
 245     }
 246 
 247     if err == nil {
 248         // a successful `read` is a failure, as it means there are
 249         // trailing JSON tokens
 250         return errors.New(`unexpected trailing data`)
 251     }
 252 
 253     // any other error, perhaps some invalid-JSON-syntax-type error
 254     return err
 255 }
 256 
 257 // handleToken handles recursion for func run
 258 func handleToken(w *bufio.Writer, dec *json.Decoder, t json.Token, path []any) error {
 259     switch t := t.(type) {
 260     case json.Delim:
 261         switch t {
 262         case json.Delim('['):
 263             return handleArray(w, dec, path)
 264         case json.Delim('{'):
 265             return handleObject(w, dec, path)
 266         default:
 267             return errors.New(`unsupported JSON syntax ` + string(t))
 268         }
 269 
 270     case nil:
 271         config.path(w, path)
 272         config.null(w)
 273         return endLine(w)
 274 
 275     case bool:
 276         config.path(w, path)
 277         config.boolean(w, t)
 278         return endLine(w)
 279 
 280     case json.Number:
 281         config.path(w, path)
 282         config.number(w, t)
 283         return endLine(w)
 284 
 285     case string:
 286         config.path(w, path)
 287         config.text(w, t)
 288         return endLine(w)
 289 
 290     default:
 291         // return fmt.Errorf(`unsupported token type %T`, t)
 292         return errors.New(`invalid JSON token`)
 293     }
 294 }
 295 
 296 // handleArray handles arrays for func handleToken
 297 func handleArray(w *bufio.Writer, dec *json.Decoder, path []any) error {
 298     config.path(w, path)
 299     w.WriteString(config.arrayDecl)
 300     if err := endLine(w); err != nil {
 301         return err
 302     }
 303 
 304     path = append(path, 0)
 305     last := len(path) - 1
 306 
 307     for i := 0; true; i++ {
 308         path[last] = i
 309 
 310         t, err := dec.Token()
 311         if err != nil {
 312             return err
 313         }
 314 
 315         if t == json.Delim(']') {
 316             return nil
 317         }
 318 
 319         err = handleToken(w, dec, t, path)
 320         if err != nil {
 321             return err
 322         }
 323     }
 324 
 325     // make the compiler happy
 326     return nil
 327 }
 328 
 329 // handleObject handles objects for func handleToken
 330 func handleObject(w *bufio.Writer, dec *json.Decoder, path []any) error {
 331     config.path(w, path)
 332     w.WriteString(config.objectDecl)
 333     if err := endLine(w); err != nil {
 334         return err
 335     }
 336 
 337     path = append(path, ``)
 338     last := len(path) - 1
 339 
 340     for i := 0; true; i++ {
 341         t, err := dec.Token()
 342         if err != nil {
 343             return err
 344         }
 345 
 346         if t == json.Delim('}') {
 347             return nil
 348         }
 349 
 350         k, ok := t.(string)
 351         if !ok {
 352             return errors.New(`expected a string for a key-value pair`)
 353         }
 354 
 355         path[last] = k
 356         if err != nil {
 357             return err
 358         }
 359 
 360         t, err = dec.Token()
 361         if err == io.EOF {
 362             return errors.New(`expected a value for a key-value pair`)
 363         }
 364 
 365         err = handleToken(w, dec, t, path)
 366         if err != nil {
 367             return err
 368         }
 369     }
 370 
 371     // make the compiler happy
 372     return nil
 373 }
 374 
 375 func monoPath(w *bufio.Writer, path []any) error {
 376     var buf [24]byte
 377 
 378     w.WriteString(`json`)
 379 
 380     for _, v := range path {
 381         switch v := v.(type) {
 382         case int:
 383             w.WriteByte('[')
 384             w.Write(strconv.AppendInt(buf[:0], int64(v), 10))
 385             w.WriteByte(']')
 386 
 387         case string:
 388             if !needsEscaping(v) {
 389                 w.WriteByte('.')
 390                 w.WriteString(v)
 391                 continue
 392             }
 393             w.WriteByte('[')
 394             monoString(w, v)
 395             w.WriteByte(']')
 396         }
 397     }
 398 
 399     w.WriteString(` = `)
 400     return nil
 401 }
 402 
 403 func monoNull(w *bufio.Writer) error {
 404     w.WriteString(`null`)
 405     return nil
 406 }
 407 
 408 func monoBool(w *bufio.Writer, b bool) error {
 409     if b {
 410         w.WriteString(`true`)
 411     } else {
 412         w.WriteString(`false`)
 413     }
 414     return nil
 415 }
 416 
 417 func monoNumber(w *bufio.Writer, n json.Number) error {
 418     w.WriteString(n.String())
 419     return nil
 420 }
 421 
 422 func monoString(w *bufio.Writer, s string) error {
 423     w.WriteByte('"')
 424     for i := range s {
 425         w.Write(escapedStringBytes[s[i]])
 426     }
 427     w.WriteByte('"')
 428     return nil
 429 }
 430 
 431 func styledPath(w *bufio.Writer, path []any) error {
 432     var buf [24]byte
 433 
 434     w.WriteString("\x1b[38;2;135;95;255mjson\x1b[0m")
 435 
 436     for _, v := range path {
 437         switch v := v.(type) {
 438         case int:
 439             w.WriteString("\x1b[38;2;168;168;168m[")
 440             w.WriteString("\x1b[38;2;0;135;95m")
 441             w.Write(strconv.AppendInt(buf[:0], int64(v), 10))
 442             w.WriteString("\x1b[38;2;168;168;168m]\x1b[0m")
 443 
 444         case string:
 445             if !needsEscaping(v) {
 446                 w.WriteString("\x1b[38;2;168;168;168m.")
 447                 w.WriteString("\x1b[38;2;135;95;255m")
 448                 w.WriteString(v)
 449                 w.WriteString("\x1b[0m")
 450                 continue
 451             }
 452 
 453             w.WriteString("\x1b[38;2;168;168;168m[")
 454             styledString(w, v)
 455             w.WriteString("\x1b[38;2;168;168;168m]\x1b[0m")
 456         }
 457     }
 458 
 459     w.WriteString(" \x1b[38;2;168;168;168m=\x1b[0m ")
 460     return nil
 461 }
 462 
 463 func styledNull(w *bufio.Writer) error {
 464     w.WriteString("\x1b[38;2;168;168;168m")
 465     w.WriteString(`null`)
 466     w.WriteString("\x1b[0m")
 467     return nil
 468 }
 469 
 470 func styledBool(w *bufio.Writer, b bool) error {
 471     if b {
 472         w.WriteString("\x1b[38;2;95;175;215mtrue\x1b[0m")
 473     } else {
 474         w.WriteString("\x1b[38;2;95;175;215mfalse\x1b[0m")
 475     }
 476     return nil
 477 }
 478 
 479 func styledNumber(w *bufio.Writer, n json.Number) error {
 480     w.WriteString("\x1b[38;2;0;135;95m")
 481     w.WriteString(n.String())
 482     w.WriteString("\x1b[0m")
 483     return nil
 484 }
 485 
 486 func styledString(w *bufio.Writer, s string) error {
 487     w.WriteString("\x1b[38;2;168;168;168m\"\x1b[0m")
 488     for i := range s {
 489         w.Write(escapedStringBytes[s[i]])
 490     }
 491     w.WriteString("\x1b[38;2;168;168;168m\"\x1b[0m")
 492     return nil
 493 }
 494 
 495 func needsEscaping(s string) bool {
 496     for _, r := range s {
 497         if r < ' ' || r > '~' {
 498             return true
 499         }
 500 
 501         switch r {
 502         case '"', '\'', '\\':
 503             return true
 504         }
 505     }
 506 
 507     return false
 508 }
 509 
 510 func endLine(w *bufio.Writer) error {
 511     w.WriteByte(';')
 512     if err := w.WriteByte('\n'); err == nil {
 513         return nil
 514     }
 515     return io.EOF
 516 }