File: zj.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 zj.go 30 */ 31 32 package main 33 34 import ( 35 "bufio" 36 "encoding/json" 37 "errors" 38 "fmt" 39 "io" 40 "os" 41 "sort" 42 "strconv" 43 "strings" 44 ) 45 46 // errNoMoreOutput is a generic dummy output-error, which is meant to be 47 // ultimately ignored, being just an excuse to quit the app immediately 48 // and successfully 49 var errNoMoreOutput = errors.New(`no more output`) 50 51 func main() { 52 if err := run(os.Args[1:]); err != nil && err != errNoMoreOutput { 53 os.Stderr.WriteString(err.Error()) 54 os.Stderr.WriteString("\n") 55 os.Exit(1) 56 } 57 } 58 59 func run(keys []string) error { 60 data, err := load(os.Stdin) 61 if err != nil { 62 return err 63 } 64 65 data, err = zoom(data, keys) 66 if err != nil { 67 return err 68 } 69 70 return output(os.Stdout, data) 71 } 72 73 // object is a map with ordered keys 74 type object struct { 75 keys []string 76 kv map[string]any 77 } 78 79 func load(r io.Reader) (any, error) { 80 dec := json.NewDecoder(r) 81 // avoid parsing numbers, so unusually-long numbers are kept verbatim, 82 // even if JSON parsers aren't required to guarantee such input-fidelity 83 // for numbers 84 dec.UseNumber() 85 86 t, err := dec.Token() 87 if err == io.EOF { 88 return nil, errors.New(`input has no JSON values`) 89 } 90 91 data, err := loadToken(dec, t) 92 if err != nil { 93 return nil, err 94 } 95 96 _, err = dec.Token() 97 if err == io.EOF { 98 // input is over, so it's a success 99 return data, nil 100 } 101 102 if err == nil { 103 // a successful `read` is a failure, as it means there are 104 // trailing JSON tokens 105 return data, errors.New(`unexpected trailing data`) 106 } 107 108 // any other error, perhaps some invalid-JSON-syntax-type error 109 return data, err 110 } 111 112 func loadToken(dec *json.Decoder, t json.Token) (any, error) { 113 switch t := t.(type) { 114 case json.Delim: 115 switch t { 116 case json.Delim('['): 117 return loadArray(dec) 118 case json.Delim('{'): 119 return loadObject(dec) 120 default: 121 return nil, errors.New(`unsupported JSON syntax`) 122 } 123 124 case nil: 125 return nil, nil 126 127 case bool: 128 return t, nil 129 130 case json.Number: 131 return t, nil 132 133 case string: 134 return t, nil 135 136 default: 137 // return fmt.Errorf(`unsupported token type %T`, t) 138 return t, errors.New(`invalid JSON token`) 139 } 140 } 141 142 func loadArray(dec *json.Decoder) ([]any, error) { 143 var data []any 144 145 for { 146 t, err := dec.Token() 147 if err == io.EOF { 148 return data, nil 149 } 150 151 if err != nil { 152 return data, err 153 } 154 155 if t == json.Delim(']') { 156 return data, nil 157 } 158 159 v, err := loadToken(dec, t) 160 if err != nil { 161 return data, err 162 } 163 data = append(data, v) 164 } 165 } 166 167 func loadObject(dec *json.Decoder) (object, error) { 168 var data object 169 data.kv = make(map[string]any) 170 171 for { 172 t, err := dec.Token() 173 if err == io.EOF { 174 return data, nil 175 } 176 177 if err != nil { 178 return data, err 179 } 180 181 if t == json.Delim('}') { 182 return data, nil 183 } 184 185 k, ok := t.(string) 186 if !ok { 187 return data, errors.New(`expected a string for a key-value pair`) 188 } 189 190 t, err = dec.Token() 191 if err == io.EOF { 192 return data, errors.New(`expected a value for a key-value pair`) 193 } 194 195 v, err := loadToken(dec, t) 196 if err != nil { 197 return data, err 198 } 199 200 if _, ok := data.kv[k]; !ok { 201 data.keys = append(data.keys, k) 202 } 203 data.kv[k] = v 204 } 205 } 206 207 var fallbackAliases = map[string]string{ 208 `.i`: `.info`, 209 `.k`: `.keys`, 210 `.l`: `.length`, 211 `.len`: `.length`, 212 `.t`: `.type`, 213 `.set`: `.unique`, 214 `.u`: `.unique`, 215 `.uniq`: `.unique`, 216 } 217 218 var fallbacks = map[string]func(v any, k string, rest []string) (any, error){ 219 `+`: pick, 220 `-`: drop, 221 `.keys`: keys, 222 `.info`: info, 223 `.length`: length, 224 `.tally`: tally, 225 `.type`: kind, 226 `.unique`: unique, 227 } 228 229 func info(data any, k string, rest []string) (any, error) { 230 switch data := data.(type) { 231 case nil: 232 return `null`, nil 233 234 case bool: 235 if data { 236 return `true (boolean)`, nil 237 } 238 return `false (boolean)`, nil 239 240 case json.Number: 241 return data.String() + ` (number)`, nil 242 243 case string: 244 c := 0 245 for range data { 246 c++ 247 } 248 return `string (` + strconv.Itoa(c) + ` runes)`, nil 249 250 case []any: 251 return `array (` + strconv.Itoa(len(data)) + ` items)`, nil 252 253 case object: 254 return `object (` + strconv.Itoa(len(data.keys)) + ` items)`, nil 255 256 default: 257 return ``, errors.New(`unsupported type`) 258 } 259 } 260 261 func keys(data any, k string, rest []string) (any, error) { 262 switch data := data.(type) { 263 default: 264 return nil, nil 265 266 case string: 267 c := 0 268 for range data { 269 c++ 270 } 271 return c, nil 272 273 case []any: 274 keys := make([]any, 0, len(data)) 275 for i := range data { 276 keys = append(keys, i) 277 } 278 return keys, nil 279 280 case object: 281 keys := make([]any, 0, len(data.keys)) 282 for _, k := range data.keys { 283 keys = append(keys, k) 284 } 285 return keys, nil 286 } 287 } 288 289 func length(data any, k string, rest []string) (any, error) { 290 switch data := data.(type) { 291 default: 292 return nil, nil 293 294 case string: 295 c := 0 296 for range data { 297 c++ 298 } 299 return c, nil 300 301 case []any: 302 return len(data), nil 303 304 case object: 305 return len(data.keys), nil 306 } 307 } 308 309 func kind(data any, k string, rest []string) (any, error) { 310 switch data.(type) { 311 case nil: 312 return `null`, nil 313 case bool: 314 return `boolean`, nil 315 case int, json.Number: 316 return `number`, nil 317 case string: 318 return `string`, nil 319 case []any: 320 return `array`, nil 321 case object: 322 return `object`, nil 323 default: 324 // return ``, errors.New(`unsupported type`) 325 return fmt.Sprintf(`%T`, data), nil 326 } 327 } 328 329 func tally(data any, k string, rest []string) (any, error) { 330 switch data := data.(type) { 331 default: 332 return nil, nil 333 case []any: 334 return tallyArray(data), nil 335 } 336 } 337 338 type tallySortable struct { 339 keys []string 340 tally map[string]int 341 } 342 343 func (ts tallySortable) Len() int { 344 return len(ts.keys) 345 } 346 347 func (ts tallySortable) Less(i, j int) bool { 348 return ts.tally[ts.keys[i]] > ts.tally[ts.keys[j]] 349 } 350 351 func (ts tallySortable) Swap(i, j int) { 352 ts.keys[i], ts.keys[j] = ts.keys[j], ts.keys[i] 353 } 354 355 func tallyArray(data []any) any { 356 var keys []string 357 tally := make(map[string]int) 358 359 for _, v := range data { 360 d, err := json.Marshal(v) 361 if err != nil { 362 continue 363 } 364 tag := string(d) 365 366 if _, ok := tally[tag]; !ok { 367 keys = append(keys, tag) 368 } 369 tally[tag]++ 370 } 371 372 var sorted object 373 sorted.kv = make(map[string]any) 374 sort.Sort(tallySortable{keys: keys, tally: tally}) 375 for _, tag := range keys { 376 sorted.keys = append(sorted.keys, tag) 377 sorted.kv[tag] = tally[tag] 378 } 379 return sorted 380 } 381 382 func unique(data any, k string, rest []string) (any, error) { 383 switch data := data.(type) { 384 default: 385 return data, nil 386 case []any: 387 return uniqueArray(data), nil 388 } 389 } 390 391 func uniqueArray(data []any) any { 392 var unique []any 393 got := make(map[string]struct{}) 394 395 for _, v := range data { 396 d, err := json.Marshal(v) 397 if err != nil { 398 continue 399 } 400 tag := string(d) 401 402 if _, ok := got[tag]; ok { 403 continue 404 } 405 406 unique = append(unique, v) 407 got[tag] = struct{}{} 408 } 409 410 return unique 411 } 412 413 func pick(data any, k string, rest []string) (any, error) { 414 switch data := data.(type) { 415 case []any: 416 return pickArray(data, rest) 417 case object: 418 return pickObject(data, rest), nil 419 default: 420 return nil, nil 421 } 422 } 423 424 func drop(data any, k string, rest []string) (any, error) { 425 switch data := data.(type) { 426 case []any: 427 return dropArray(data, rest) 428 case object: 429 return dropObject(data, uniqueStrings(rest)), nil 430 default: 431 return nil, nil 432 } 433 } 434 435 func dealias(s string, aliases map[string]string) string { 436 if alias, ok := aliases[s]; ok { 437 return alias 438 } 439 return s 440 } 441 442 func zoom(data any, keys []string) (any, error) { 443 for i, k := range keys { 444 switch v := data.(type) { 445 case []any: 446 if i, ok := matchIndex(k, len(v)); ok { 447 data = v[i] 448 continue 449 } 450 451 if i, j, ok := matchSlice(k, len(v)); ok { 452 if len(v) > 0 { 453 data = v[i:j] 454 } 455 continue 456 } 457 458 if f, ok := fallbacks[dealias(k, fallbackAliases)]; ok { 459 v, err := f(data, k, keys[i+1:]) 460 if err != nil { 461 return v, err 462 } 463 data = v 464 continue 465 } 466 467 // data = nil 468 return data, errors.New(`arrays have no properties`) 469 470 case object: 471 if key, ok := matchKey(v, k); ok { 472 data = v.kv[key] 473 continue 474 } 475 476 if i, j, ok := matchSlice(k, len(v.keys)); ok { 477 if len(v.keys) > 0 { 478 v.keys = v.keys[i:j] 479 data = v 480 } 481 continue 482 } 483 484 if f, ok := fallbacks[dealias(k, fallbackAliases)]; ok { 485 v, err := f(data, k, keys[i+1:]) 486 if err != nil { 487 return v, err 488 } 489 data = v 490 continue 491 } 492 493 data = nil 494 495 default: 496 // return nil, errors.New(`can only zoom on arrays/objects`) 497 data = nil 498 } 499 } 500 501 return data, nil 502 } 503 504 func matchIndex(k string, length int) (int, bool) { 505 if i, err := strconv.Atoi(k); err == nil { 506 if i < 0 { 507 i += length 508 } 509 510 if 0 <= i && i < length { 511 return i, true 512 } 513 } 514 515 return -1, false 516 } 517 518 func matchKey(data object, k string) (string, bool) { 519 // try exact key-match 520 if _, ok := data.kv[k]; ok { 521 return k, true 522 } 523 524 // try a case-insensitive key-match 525 for _, dk := range data.keys { 526 if strings.EqualFold(k, dk) { 527 return dk, true 528 } 529 } 530 531 // try number-indexing 532 if i, err := strconv.Atoi(k); err == nil { 533 if i < 0 { 534 i += len(data.keys) 535 } 536 537 if 0 <= i && i < len(data.keys) { 538 return data.keys[i], true 539 } else { 540 return ``, false 541 } 542 } 543 544 return ``, false 545 } 546 547 func matchSlice(k string, length int) (int, int, bool) { 548 k = strings.TrimSpace(k) 549 colon := strings.IndexByte(k, ':') 550 551 // when there's no colon in it, key isn't a slice for sure 552 if colon < 0 { 553 return 0, 0, false 554 } 555 556 // can't slice an empty container 557 if length == 0 { 558 return 0, 0, false 559 } 560 561 // when given `:` 562 if len(k) == 1 { 563 return 0, length, true 564 } 565 566 // when slice starts with `:`, it implies starting from 0 567 if colon == 0 { 568 if end, ok := matchIndex(k[colon+1:], length); ok { 569 return 0, end, true 570 } 571 return 0, 0, false 572 } 573 574 // when slice ends with `:`, it implies until the end 575 if colon == len(k)-1 { 576 if start, ok := matchIndex(k[:colon], length); ok { 577 return start, length, true 578 } 579 return 0, 0, false 580 } 581 582 start, ok := matchIndex(k[:colon], length) 583 if !ok { 584 return 0, 0, false 585 } 586 587 end, ok := matchIndex(k[colon+1:], length) 588 if !ok { 589 return 0, 0, false 590 } 591 592 // backward/reversed slices aren't supported 593 if start > end { 594 return 0, 0, false 595 } 596 597 // restrict slices to valid ranges 598 if start < 0 { 599 start = 0 600 } 601 if end > length { 602 end = length 603 } 604 return start, end, true 605 } 606 607 func pickArray(data []any, keys []string) ([]any, error) { 608 var res []any 609 for _, v := range data { 610 if o, ok := v.(object); ok { 611 res = append(res, pickObject(o, keys)) 612 } 613 } 614 return res, nil 615 } 616 617 func dropArray(data []any, keys []string) ([]any, error) { 618 var res []any 619 avoid := uniqueStrings(keys) 620 for _, v := range data { 621 if o, ok := v.(object); ok { 622 res = append(res, dropObject(o, avoid)) 623 } 624 } 625 return res, nil 626 } 627 628 func pickObject(data object, keys []string) object { 629 var o object 630 o.kv = make(map[string]any) 631 for _, k := range keys { 632 if k, ok := matchKey(data, k); ok { 633 o.keys = append(o.keys, k) 634 o.kv[k] = data.kv[k] 635 } 636 } 637 return o 638 } 639 640 func dropObject(data object, avoid map[string]struct{}) object { 641 var o object 642 o.kv = make(map[string]any) 643 for _, k := range data.keys { 644 if _, ok := avoid[k]; !ok { 645 o.keys = append(o.keys, k) 646 o.kv[k] = data.kv[k] 647 } 648 } 649 return o 650 } 651 652 func uniqueStrings(values []string) map[string]struct{} { 653 unique := make(map[string]struct{}) 654 for _, v := range values { 655 unique[v] = struct{}{} 656 } 657 return unique 658 } 659 660 // escapedStringBytes helps func outputString treat all string bytes quickly 661 // and correctly, using their officially-supported JSON escape sequences 662 // 663 // https://www.rfc-editor.org/rfc/rfc8259#section-7 664 var escapedStringBytes = [256][]byte{ 665 {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'}, 666 {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'}, 667 {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'}, 668 {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'}, 669 {'\\', 'b'}, {'\\', 't'}, 670 {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'}, 671 {'\\', 'f'}, {'\\', 'r'}, 672 {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'}, 673 {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'}, 674 {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'}, 675 {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'}, 676 {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'}, 677 {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'}, 678 {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'}, 679 {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'}, 680 {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'}, 681 {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39}, 682 {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47}, 683 {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55}, 684 {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63}, 685 {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71}, 686 {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79}, 687 {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87}, 688 {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95}, 689 {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103}, 690 {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111}, 691 {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119}, 692 {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127}, 693 {128}, {129}, {130}, {131}, {132}, {133}, {134}, {135}, 694 {136}, {137}, {138}, {139}, {140}, {141}, {142}, {143}, 695 {144}, {145}, {146}, {147}, {148}, {149}, {150}, {151}, 696 {152}, {153}, {154}, {155}, {156}, {157}, {158}, {159}, 697 {160}, {161}, {162}, {163}, {164}, {165}, {166}, {167}, 698 {168}, {169}, {170}, {171}, {172}, {173}, {174}, {175}, 699 {176}, {177}, {178}, {179}, {180}, {181}, {182}, {183}, 700 {184}, {185}, {186}, {187}, {188}, {189}, {190}, {191}, 701 {192}, {193}, {194}, {195}, {196}, {197}, {198}, {199}, 702 {200}, {201}, {202}, {203}, {204}, {205}, {206}, {207}, 703 {208}, {209}, {210}, {211}, {212}, {213}, {214}, {215}, 704 {216}, {217}, {218}, {219}, {220}, {221}, {222}, {223}, 705 {224}, {225}, {226}, {227}, {228}, {229}, {230}, {231}, 706 {232}, {233}, {234}, {235}, {236}, {237}, {238}, {239}, 707 {240}, {241}, {242}, {243}, {244}, {245}, {246}, {247}, 708 {248}, {249}, {250}, {251}, {252}, {253}, {254}, {255}, 709 } 710 711 // writeSpaces does what it says, minimizing calls to write-like funcs 712 func writeSpaces(w *bufio.Writer, n int) { 713 const spaces = ` ` 714 if n < 1 { 715 return 716 } 717 718 for n >= len(spaces) { 719 w.WriteString(spaces) 720 n -= len(spaces) 721 } 722 w.WriteString(spaces[:n]) 723 } 724 725 func output(w io.Writer, data any) error { 726 bw := bufio.NewWriter(w) 727 defer bw.Flush() 728 729 if err := outputValue(bw, data, 0, 0); err != nil { 730 return err 731 } 732 bw.WriteRune('\n') 733 return nil 734 } 735 736 func outputValue(w *bufio.Writer, data any, pre, level int) error { 737 switch data := data.(type) { 738 case nil: 739 w.WriteString(`null`) 740 return nil 741 742 case bool: 743 if data { 744 w.WriteString(`true`) 745 } else { 746 w.WriteString(`false`) 747 } 748 return nil 749 750 case int: 751 var buf [24]byte 752 w.Write(strconv.AppendInt(buf[:0], int64(data), 10)) 753 return nil 754 755 case json.Number: 756 writeSpaces(w, 2*pre) 757 w.WriteString(data.String()) 758 return nil 759 760 case string: 761 return outputString(w, data, pre) 762 763 case []any: 764 return outputArray(w, data, pre, level+1) 765 766 case object: 767 return outputObject(w, data, pre, level+1) 768 769 default: 770 return errors.New(`unexpected value type`) 771 } 772 } 773 774 func outputArray(w *bufio.Writer, data []any, pre, level int) error { 775 writeSpaces(w, 2*pre) 776 w.WriteByte('[') 777 778 for i, v := range data { 779 if i > 0 { 780 _, err := w.WriteString(",\n") 781 if err != nil { 782 return errNoMoreOutput 783 } 784 } else { 785 w.WriteByte('\n') 786 } 787 788 if err := outputValue(w, v, level, level); err != nil { 789 return err 790 } 791 } 792 793 endComposite(len(data), w, level-1, ']') 794 return nil 795 } 796 797 func outputObject(w *bufio.Writer, data object, pre, level int) error { 798 writeSpaces(w, 2*pre) 799 w.WriteByte('{') 800 801 for i, k := range data.keys { 802 if i > 0 { 803 _, err := w.WriteString(",\n") 804 if err != nil { 805 return errNoMoreOutput 806 } 807 } else { 808 w.WriteByte('\n') 809 } 810 811 if err := outputString(w, k, level); err != nil { 812 return err 813 } 814 815 w.WriteString(": ") 816 817 if err := outputValue(w, data.kv[k], 0, level); err != nil { 818 return err 819 } 820 } 821 822 endComposite(len(data.keys), w, pre, '}') 823 return nil 824 } 825 826 func outputString(w *bufio.Writer, s string, level int) error { 827 writeSpaces(w, 2*level) 828 w.WriteByte('"') 829 for i := range s { 830 w.Write(escapedStringBytes[s[i]]) 831 } 832 w.WriteByte('"') 833 return nil 834 } 835 836 // endComposite handles common closing logic which is used twice both in each 837 // of func handleArray and func handleObject 838 func endComposite(i int, w *bufio.Writer, pre int, end byte) { 839 if i > 0 { 840 w.WriteByte('\n') 841 writeSpaces(w, 2*pre) 842 } 843 w.WriteByte(end) 844 }