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