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