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