@@ -0,0 +1,277 @@
10+ "github.com/itchyny/gojq"
11+ "mvdan.cc/sh/v3/interp"
14+const jqUsage = `jq - Go implementation of jq (gojq 0.12.19 builtin)
17+ %% echo '{"foo": 128}' | jq '.foo'
20+ jq [OPTIONS] [FILTER] [FILE...]
23+ -r, --raw-output output raw strings
24+ -j, --join-output implies -r with no newline delimiter
25+ -c, --compact-output output without pretty-printing
26+ -s, --slurp read all inputs into an array
27+ -n, --null-input use null as input value
28+ -e, --exit-status exit 1 when the last value is false or null
29+ -R, --raw-input read input as raw strings
30+ --arg name value set a string value to a variable
31+ --argjson name value set a JSON value to a variable
32+ -h, --help display this help
35+// handleJQ implements the jq builtin using gojq. It supports a subset of jq
36+// flags: -r (raw output), -c (compact output), -s (slurp), -n (null input),
37+// -e (exit status), -R (raw input), and --arg name value.
39+// Note that this is somewhat of a reimplmentation of the CLI of the glorious
40+// github.com/itchyny/gojq, and we'd ideally get the CLI exposed upstream to
41+// avoid this falling out of sync.
42+func handleJQ(args []string, stdin io.Reader, stdout, stderr io.Writer) error {
55+ // Parse flags and extract the query.
57+ var fileArgs []string
62+ case arg == "-h" || arg == "--help":
63+ fmt.Fprint(stdout, jqUsage)
65+ case arg == "-r" || arg == "--raw-output":
67+ case arg == "-j" || arg == "--join-output":
70+ case arg == "-c" || arg == "--compact-output":
72+ case arg == "-s" || arg == "--slurp":
74+ case arg == "-n" || arg == "--null-input":
76+ case arg == "-e" || arg == "--exit-status":
78+ case arg == "-R" || arg == "--raw-input":
81+ if i+2 >= len(args) {
82+ fmt.Fprintf(stderr, "jq: --arg requires name and value\n")
83+ return interp.ExitStatus(2)
85+ argNames = append(argNames, "$"+args[i+1])
86+ argValues = append(argValues, args[i+2])
88+ case arg == "--argjson":
89+ if i+2 >= len(args) {
90+ fmt.Fprintf(stderr, "jq: --argjson requires name and value\n")
91+ return interp.ExitStatus(2)
94+ if err := json.Unmarshal([]byte(args[i+2]), &val); err != nil {
95+ fmt.Fprintf(stderr, "jq: invalid JSON for --argjson %s: %s\n", args[i+1], err)
96+ return interp.ExitStatus(2)
98+ argNames = append(argNames, "$"+args[i+1])
99+ argValues = append(argValues, val)
103+ // Remaining args are file arguments.
105+ fileArgs = append(fileArgs, args[i])
109+ case strings.HasPrefix(arg, "-") && queryStr != "":
110+ fmt.Fprintf(stderr, "jq: unknown option: %s\n", arg)
111+ return interp.ExitStatus(2)
116+ fileArgs = append(fileArgs, arg)
126+ query, err := gojq.Parse(queryStr)
128+ fmt.Fprintf(stderr, "jq: %s\n", err)
129+ return interp.ExitStatus(3)
132+ opts := []gojq.CompilerOption{
133+ gojq.WithEnvironLoader(os.Environ),
135+ if len(argNames) > 0 {
136+ opts = append(opts, gojq.WithVariables(argNames))
139+ code, err := gojq.Compile(query, opts...)
141+ fmt.Fprintf(stderr, "jq: %s\n", err)
142+ return interp.ExitStatus(3)
145+ // Build input values.
146+ inputs, err := readInputs(stdin, fileArgs, nullInput, rawInput, slurp)
148+ fmt.Fprintf(stderr, "jq: %s\n", err)
149+ return interp.ExitStatus(2)
153+ for _, input := range inputs {
154+ iter := code.Run(input, argValues...)
156+ v, ok := iter.Next()
160+ if err, ok := v.(error); ok {
161+ fmt.Fprintf(stderr, "jq: %s\n", err)
162+ return interp.ExitStatus(5)
165+ lastFalsy = v == nil || v == false
167+ if err := writeValue(stdout, v, rawOutput, compact, joinOutput); err != nil {
173+ if exitStatus && lastFalsy {
174+ return interp.ExitStatus(1)
179+// readInputs reads JSON (or raw) input values from stdin or files.
180+func readInputs(stdin io.Reader, files []string, nullInput, rawInput, slurp bool) ([]any, error) {
182+ return []any{nil}, nil
185+ var readers []io.Reader
187+ for _, f := range files {
188+ file, err := os.Open(f)
193+ readers = append(readers, file)
196+ readers = []io.Reader{stdin}
200+ for _, r := range readers {
201+ data, err := io.ReadAll(r)
207+ lines := strings.Split(string(data), "\n")
209+ vals = append(vals, strings.Join(lines, "\n"))
211+ for _, line := range lines {
212+ if line != "" || !slurp {
213+ vals = append(vals, line)
220+ // Decode potentially multiple JSON values from the stream.
221+ dec := json.NewDecoder(strings.NewReader(string(data)))
222+ var streamVals []any
225+ if err := dec.Decode(&v); err != nil {
229+ return nil, fmt.Errorf("parse error: %w", err)
231+ streamVals = append(streamVals, v)
235+ vals = append(vals, streamVals)
237+ vals = append(vals, streamVals...)
242+ return []any{nil}, nil
247+// writeValue writes a single jq output value.
248+func writeValue(w io.Writer, v any, raw, compact, join bool) error {
250+ if s, ok := v.(string); ok {
251+ if _, err := fmt.Fprint(w, s); err != nil {
255+ _, err := fmt.Fprint(w, "\n")
265+ bs, err = gojq.Marshal(v)
267+ bs, err = json.MarshalIndent(v, "", " ")
272+ if _, writeErr := w.Write(bs); writeErr != nil {
275+ _, err = fmt.Fprint(w, "\n")