/* The MIT License (MIT) Copyright © 2020-2025 pacman64 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* Single-file source-code for zj. To compile a smaller-sized command-line app, you can use the `go` command as follows: go build -ldflags "-s -w" -trimpath zj.go */ package main import ( "bufio" "encoding/json" "errors" "fmt" "io" "os" "sort" "strconv" "strings" ) // errNoMoreOutput is a generic dummy output-error, which is meant to be // ultimately ignored, being just an excuse to quit the app immediately // and successfully var errNoMoreOutput = errors.New(`no more output`) func main() { if err := run(os.Args[1:]); err != nil && err != errNoMoreOutput { os.Stderr.WriteString("\x1b[31m") os.Stderr.WriteString(err.Error()) os.Stderr.WriteString("\x1b[0m\n") os.Exit(1) } } func run(keys []string) error { data, err := load(os.Stdin) if err != nil { return err } data, err = zoom(data, keys) if err != nil { return err } return output(os.Stdout, data) } // object is a map with ordered keys type object struct { keys []string kv map[string]any } func load(r io.Reader) (any, error) { dec := json.NewDecoder(r) // avoid parsing numbers, so unusually-long numbers are kept verbatim, // even if JSON parsers aren't required to guarantee such input-fidelity // for numbers dec.UseNumber() t, err := dec.Token() if err == io.EOF { return nil, errors.New(`input has no JSON values`) } data, err := loadToken(dec, t) if err != nil { return nil, err } _, err = dec.Token() if err == io.EOF { // input is over, so it's a success return data, nil } if err == nil { // a successful `read` is a failure, as it means there are // trailing JSON tokens return data, errors.New(`unexpected trailing data`) } // any other error, perhaps some invalid-JSON-syntax-type error return data, err } func loadToken(dec *json.Decoder, t json.Token) (any, error) { switch t := t.(type) { case json.Delim: switch t { case json.Delim('['): return loadArray(dec) case json.Delim('{'): return loadObject(dec) default: return nil, errors.New(`unsupported JSON syntax`) } case nil: return nil, nil case bool: return t, nil case json.Number: return t, nil case string: return t, nil default: // return fmt.Errorf(`unsupported token type %T`, t) return t, errors.New(`invalid JSON token`) } } func loadArray(dec *json.Decoder) ([]any, error) { var data []any for { t, err := dec.Token() if err == io.EOF { return data, nil } if err != nil { return data, err } if t == json.Delim(']') { return data, nil } v, err := loadToken(dec, t) if err != nil { return data, err } data = append(data, v) } } func loadObject(dec *json.Decoder) (object, error) { var data object data.kv = make(map[string]any) for { t, err := dec.Token() if err == io.EOF { return data, nil } if err != nil { return data, err } if t == json.Delim('}') { return data, nil } k, ok := t.(string) if !ok { return data, errors.New(`expected a string for a key-value pair`) } t, err = dec.Token() if err == io.EOF { return data, errors.New(`expected a value for a key-value pair`) } v, err := loadToken(dec, t) if err != nil { return data, err } if _, ok := data.kv[k]; !ok { data.keys = append(data.keys, k) } data.kv[k] = v } } var fallbackAliases = map[string]string{ `.i`: `.info`, `.k`: `.keys`, `.l`: `.length`, `.len`: `.length`, `.t`: `.type`, `.set`: `.unique`, `.u`: `.unique`, `.uniq`: `.unique`, } var fallbacks = map[string]func(v any, k string, rest []string) (any, error){ `+`: pick, `-`: drop, `.keys`: keys, `.info`: info, `.length`: length, `.tally`: tally, `.type`: kind, `.unique`: unique, } func info(data any, k string, rest []string) (any, error) { switch data := data.(type) { case nil: return `null`, nil case bool: if data { return `true (boolean)`, nil } return `false (boolean)`, nil case json.Number: return data.String() + ` (number)`, nil case string: c := 0 for range data { c++ } return `string (` + strconv.Itoa(c) + ` runes)`, nil case []any: return `array (` + strconv.Itoa(len(data)) + ` items)`, nil case object: return `object (` + strconv.Itoa(len(data.keys)) + ` items)`, nil default: return ``, errors.New(`unsupported type`) } } func keys(data any, k string, rest []string) (any, error) { switch data := data.(type) { default: return nil, nil case string: c := 0 for range data { c++ } return c, nil case []any: keys := make([]any, 0, len(data)) for i := range data { keys = append(keys, i) } return keys, nil case object: keys := make([]any, 0, len(data.keys)) for _, k := range data.keys { keys = append(keys, k) } return keys, nil } } func length(data any, k string, rest []string) (any, error) { switch data := data.(type) { default: return nil, nil case string: c := 0 for range data { c++ } return c, nil case []any: return len(data), nil case object: return len(data.keys), nil } } func kind(data any, k string, rest []string) (any, error) { switch data.(type) { case nil: return `null`, nil case bool: return `boolean`, nil case int, json.Number: return `number`, nil case string: return `string`, nil case []any: return `array`, nil case object: return `object`, nil default: // return ``, errors.New(`unsupported type`) return fmt.Sprintf(`%T`, data), nil } } func tally(data any, k string, rest []string) (any, error) { switch data := data.(type) { default: return nil, nil case []any: return tallyArray(data), nil } } type tallySortable struct { keys []string tally map[string]int } func (ts tallySortable) Len() int { return len(ts.keys) } func (ts tallySortable) Less(i, j int) bool { return ts.tally[ts.keys[i]] > ts.tally[ts.keys[j]] } func (ts tallySortable) Swap(i, j int) { ts.keys[i], ts.keys[j] = ts.keys[j], ts.keys[i] } func tallyArray(data []any) any { var keys []string tally := make(map[string]int) for _, v := range data { d, err := json.Marshal(v) if err != nil { continue } tag := string(d) if _, ok := tally[tag]; !ok { keys = append(keys, tag) } tally[tag]++ } var sorted object sorted.kv = make(map[string]any) sort.Sort(tallySortable{keys: keys, tally: tally}) for _, tag := range keys { sorted.keys = append(sorted.keys, tag) sorted.kv[tag] = tally[tag] } return sorted } func unique(data any, k string, rest []string) (any, error) { switch data := data.(type) { default: return data, nil case []any: return uniqueArray(data), nil } } func uniqueArray(data []any) any { var unique []any got := make(map[string]struct{}) for _, v := range data { d, err := json.Marshal(v) if err != nil { continue } tag := string(d) if _, ok := got[tag]; ok { continue } unique = append(unique, v) got[tag] = struct{}{} } return unique } func pick(data any, k string, rest []string) (any, error) { switch data := data.(type) { case []any: return pickArray(data, rest) case object: return pickObject(data, rest), nil default: return nil, nil } } func drop(data any, k string, rest []string) (any, error) { switch data := data.(type) { case []any: return dropArray(data, rest) case object: return dropObject(data, uniqueStrings(rest)), nil default: return nil, nil } } func dealias(s string, aliases map[string]string) string { if alias, ok := aliases[s]; ok { return alias } return s } func zoom(data any, keys []string) (any, error) { for i, k := range keys { switch v := data.(type) { case []any: if i, ok := matchIndex(k, len(v)); ok { data = v[i] continue } if i, j, ok := matchSlice(k, len(v)); ok { if len(v) > 0 { data = v[i:j] } continue } if f, ok := fallbacks[dealias(k, fallbackAliases)]; ok { v, err := f(data, k, keys[i+1:]) if err != nil { return v, err } data = v continue } // data = nil return data, errors.New(`arrays have no properties`) case object: if key, ok := matchKey(v, k); ok { data = v.kv[key] continue } if i, j, ok := matchSlice(k, len(v.keys)); ok { if len(v.keys) > 0 { v.keys = v.keys[i:j] data = v } continue } if f, ok := fallbacks[dealias(k, fallbackAliases)]; ok { v, err := f(data, k, keys[i+1:]) if err != nil { return v, err } data = v continue } data = nil default: // return nil, errors.New(`can only zoom on arrays/objects`) data = nil } } return data, nil } func matchIndex(k string, length int) (int, bool) { if i, err := strconv.Atoi(k); err == nil { if i < 0 { i += length } if 0 <= i && i < length { return i, true } } return -1, false } func matchKey(data object, k string) (string, bool) { // try exact key-match if _, ok := data.kv[k]; ok { return k, true } // try a case-insensitive key-match for _, dk := range data.keys { if strings.EqualFold(k, dk) { return dk, true } } // try number-indexing if i, err := strconv.Atoi(k); err == nil { if i < 0 { i += len(data.keys) } if 0 <= i && i < len(data.keys) { return data.keys[i], true } else { return ``, false } } return ``, false } func matchSlice(k string, length int) (int, int, bool) { k = strings.TrimSpace(k) colon := strings.IndexByte(k, ':') // when there's no colon in it, key isn't a slice for sure if colon < 0 { return 0, 0, false } // can't slice an empty container if length == 0 { return 0, 0, false } // when given `:` if len(k) == 1 { return 0, length, true } // when slice starts with `:`, it implies starting from 0 if colon == 0 { if end, ok := matchIndex(k[colon+1:], length); ok { return 0, end, true } return 0, 0, false } // when slice ends with `:`, it implies until the end if colon == len(k)-1 { if start, ok := matchIndex(k[:colon], length); ok { return start, length, true } return 0, 0, false } start, ok := matchIndex(k[:colon], length) if !ok { return 0, 0, false } end, ok := matchIndex(k[colon+1:], length) if !ok { return 0, 0, false } // backward/reversed slices aren't supported if start > end { return 0, 0, false } // restrict slices to valid ranges if start < 0 { start = 0 } if end > length { end = length } return start, end, true } func pickArray(data []any, keys []string) ([]any, error) { var res []any for _, v := range data { if o, ok := v.(object); ok { res = append(res, pickObject(o, keys)) } } return res, nil } func dropArray(data []any, keys []string) ([]any, error) { var res []any avoid := uniqueStrings(keys) for _, v := range data { if o, ok := v.(object); ok { res = append(res, dropObject(o, avoid)) } } return res, nil } func pickObject(data object, keys []string) object { var o object o.kv = make(map[string]any) for _, k := range keys { if k, ok := matchKey(data, k); ok { o.keys = append(o.keys, k) o.kv[k] = data.kv[k] } } return o } func dropObject(data object, avoid map[string]struct{}) object { var o object o.kv = make(map[string]any) for _, k := range data.keys { if _, ok := avoid[k]; !ok { o.keys = append(o.keys, k) o.kv[k] = data.kv[k] } } return o } func uniqueStrings(values []string) map[string]struct{} { unique := make(map[string]struct{}) for _, v := range values { unique[v] = struct{}{} } return unique } // escapedStringBytes helps func outputString treat all string bytes quickly // and correctly, using their officially-supported JSON escape sequences // // https://www.rfc-editor.org/rfc/rfc8259#section-7 var escapedStringBytes = [256][]byte{ {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'}, {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'}, {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'}, {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'}, {'\\', 'b'}, {'\\', 't'}, {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'}, {'\\', 'f'}, {'\\', 'r'}, {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'}, {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'}, {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'}, {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'}, {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'}, {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'}, {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'}, {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'}, {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'}, {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39}, {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47}, {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55}, {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63}, {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71}, {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79}, {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87}, {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95}, {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103}, {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111}, {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119}, {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127}, {128}, {129}, {130}, {131}, {132}, {133}, {134}, {135}, {136}, {137}, {138}, {139}, {140}, {141}, {142}, {143}, {144}, {145}, {146}, {147}, {148}, {149}, {150}, {151}, {152}, {153}, {154}, {155}, {156}, {157}, {158}, {159}, {160}, {161}, {162}, {163}, {164}, {165}, {166}, {167}, {168}, {169}, {170}, {171}, {172}, {173}, {174}, {175}, {176}, {177}, {178}, {179}, {180}, {181}, {182}, {183}, {184}, {185}, {186}, {187}, {188}, {189}, {190}, {191}, {192}, {193}, {194}, {195}, {196}, {197}, {198}, {199}, {200}, {201}, {202}, {203}, {204}, {205}, {206}, {207}, {208}, {209}, {210}, {211}, {212}, {213}, {214}, {215}, {216}, {217}, {218}, {219}, {220}, {221}, {222}, {223}, {224}, {225}, {226}, {227}, {228}, {229}, {230}, {231}, {232}, {233}, {234}, {235}, {236}, {237}, {238}, {239}, {240}, {241}, {242}, {243}, {244}, {245}, {246}, {247}, {248}, {249}, {250}, {251}, {252}, {253}, {254}, {255}, } // writeSpaces does what it says, minimizing calls to write-like funcs func writeSpaces(w *bufio.Writer, n int) { const spaces = ` ` if n < 1 { return } for n >= len(spaces) { w.WriteString(spaces) n -= len(spaces) } w.WriteString(spaces[:n]) } func output(w io.Writer, data any) error { bw := bufio.NewWriter(w) defer bw.Flush() if err := outputValue(bw, data, 0, 0); err != nil { return err } bw.WriteRune('\n') return nil } func outputValue(w *bufio.Writer, data any, pre, level int) error { switch data := data.(type) { case nil: w.WriteString(`null`) return nil case bool: if data { w.WriteString(`true`) } else { w.WriteString(`false`) } return nil case int: var buf [24]byte w.Write(strconv.AppendInt(buf[:0], int64(data), 10)) return nil case json.Number: writeSpaces(w, 2*pre) w.WriteString(data.String()) return nil case string: return outputString(w, data, pre) case []any: return outputArray(w, data, pre, level+1) case object: return outputObject(w, data, pre, level+1) default: return errors.New(`unexpected value type`) } } func outputArray(w *bufio.Writer, data []any, pre, level int) error { writeSpaces(w, 2*pre) w.WriteByte('[') for i, v := range data { if i > 0 { _, err := w.WriteString(",\n") if err != nil { return errNoMoreOutput } } else { w.WriteByte('\n') } if err := outputValue(w, v, level, level); err != nil { return err } } endComposite(len(data), w, level-1, ']') return nil } func outputObject(w *bufio.Writer, data object, pre, level int) error { writeSpaces(w, 2*pre) w.WriteByte('{') for i, k := range data.keys { if i > 0 { _, err := w.WriteString(",\n") if err != nil { return errNoMoreOutput } } else { w.WriteByte('\n') } if err := outputString(w, k, level); err != nil { return err } w.WriteString(": ") if err := outputValue(w, data.kv[k], 0, level); err != nil { return err } } endComposite(len(data.keys), w, pre, '}') return nil } func outputString(w *bufio.Writer, s string, level int) error { writeSpaces(w, 2*level) w.WriteByte('"') for i := range s { w.Write(escapedStringBytes[s[i]]) } w.WriteByte('"') return nil } // endComposite handles common closing logic which is used twice both in each // of func handleArray and func handleObject func endComposite(i int, w *bufio.Writer, pre int, end byte) { if i > 0 { w.WriteByte('\n') writeSpaces(w, 2*pre) } w.WriteByte(end) }