1
2 package jq
3
4 import (
5 "bytes"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "io"
10 "math"
11 "os"
12 "strconv"
13 "strings"
14
15 "github.com/cli/go-gh/v2/pkg/jsonpretty"
16 "github.com/itchyny/gojq"
17 )
18
19
20
21
22
23 func Evaluate(input io.Reader, output io.Writer, expr string) error {
24 return EvaluateFormatted(input, output, expr, "", false)
25 }
26
27
28
29
30
31 func EvaluateFormatted(input io.Reader, output io.Writer, expr string, indent string, colorize bool) error {
32 query, err := gojq.Parse(expr)
33 if err != nil {
34 var e *gojq.ParseError
35 if errors.As(err, &e) {
36 str, line, column := getLineColumn(expr, e.Offset-len(e.Token))
37 return fmt.Errorf(
38 "failed to parse jq expression (line %d, column %d)\n %s\n %*c %w",
39 line, column, str, column, '^', err,
40 )
41 }
42 return err
43 }
44
45 code, err := gojq.Compile(
46 query,
47 gojq.WithEnvironLoader(func() []string {
48 return os.Environ()
49 }))
50 if err != nil {
51 return err
52 }
53
54 jsonData, err := io.ReadAll(input)
55 if err != nil {
56 return err
57 }
58
59 var responseData interface{}
60 err = json.Unmarshal(jsonData, &responseData)
61 if err != nil {
62 return err
63 }
64
65 enc := prettyEncoder{
66 w: output,
67 indent: indent,
68 colorize: colorize,
69 }
70
71 iter := code.Run(responseData)
72 for {
73 v, ok := iter.Next()
74 if !ok {
75 break
76 }
77 if err, isErr := v.(error); isErr {
78 var e *gojq.HaltError
79 if errors.As(err, &e) && e.Value() == nil {
80 break
81 }
82 return err
83 }
84 if text, e := jsonScalarToString(v); e == nil {
85 _, err := fmt.Fprintln(output, text)
86 if err != nil {
87 return err
88 }
89 } else {
90 if err = enc.Encode(v); err != nil {
91 return err
92 }
93 }
94 }
95
96 return nil
97 }
98
99 func jsonScalarToString(input interface{}) (string, error) {
100 switch tt := input.(type) {
101 case string:
102 return tt, nil
103 case float64:
104 if math.Trunc(tt) == tt {
105 return strconv.FormatFloat(tt, 'f', 0, 64), nil
106 } else {
107 return strconv.FormatFloat(tt, 'f', 2, 64), nil
108 }
109 case nil:
110 return "", nil
111 case bool:
112 return fmt.Sprintf("%v", tt), nil
113 default:
114 return "", fmt.Errorf("cannot convert type to string: %v", tt)
115 }
116 }
117
118 type prettyEncoder struct {
119 w io.Writer
120 indent string
121 colorize bool
122 }
123
124 func (p prettyEncoder) Encode(v any) error {
125 var b []byte
126 var err error
127 if p.indent == "" {
128 b, err = json.Marshal(v)
129 } else {
130 b, err = json.MarshalIndent(v, "", p.indent)
131 }
132 if err != nil {
133 return err
134 }
135 if !p.colorize {
136 if _, err := p.w.Write(b); err != nil {
137 return err
138 }
139 if _, err := p.w.Write([]byte{'\n'}); err != nil {
140 return err
141 }
142 return nil
143 }
144 return jsonpretty.Format(p.w, bytes.NewReader(b), p.indent, true)
145 }
146
147 func getLineColumn(expr string, offset int) (string, int, int) {
148 for line := 1; ; line++ {
149 index := strings.Index(expr, "\n")
150 if index < 0 {
151 return expr, line, offset + 1
152 }
153 if index >= offset {
154 return expr[:index], line, offset + 1
155 }
156 expr = expr[index+1:]
157 offset -= index + 1
158 }
159 }
160
View as plain text