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 }