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 }