X Tutup
Skip to content

Commit dbf1145

Browse files
authored
Merge pull request cli#3012 from cli/api-jq
Add `--filter` to api command to filter data using jq syntax
2 parents 0aebfac + 4e24f36 commit dbf1145

File tree

7 files changed

+270
-8
lines changed

7 files changed

+270
-8
lines changed

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ require (
1515
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
1616
github.com/hashicorp/go-version v1.2.1
1717
github.com/henvic/httpretty v0.0.6
18+
github.com/itchyny/gojq v0.12.1
1819
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
1920
github.com/mattn/go-colorable v0.1.8
2021
github.com/mattn/go-isatty v0.0.12
@@ -31,7 +32,7 @@ require (
3132
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
3233
golang.org/x/sync v0.0.0-20190423024810-112230192c58
3334
golang.org/x/text v0.3.4 // indirect
34-
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
35+
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
3536
)
3637

3738
replace github.com/shurcooL/graphql => github.com/cli/shurcooL-graphql v0.0.0-20200707151639-0f7232a2bf7e

go.sum

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,13 @@ github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDG
143143
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
144144
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
145145
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
146+
github.com/itchyny/astgen-go v0.0.0-20210113000433-0da0671862a3 h1:l7vogWrq+zj8v5t/G69/eT13nAGs2H7cq+CI2nlnKdk=
147+
github.com/itchyny/astgen-go v0.0.0-20210113000433-0da0671862a3/go.mod h1:296z3W7Xsrp2mlIY88ruDKscuvrkL6zXCNRtaYVshzw=
148+
github.com/itchyny/go-flags v1.5.0/go.mod h1:lenkYuCobuxLBAd/HGFE4LRoW8D3B6iXRQfWYJ+MNbA=
149+
github.com/itchyny/gojq v0.12.1 h1:pQJrG8LXgEbZe9hvpfjKg7UlBfieQQydIw3YQq+7WIA=
150+
github.com/itchyny/gojq v0.12.1/go.mod h1:Y5Lz0qoT54ii+ucY/K3yNDy19qzxZvWNBMBpKUDQR/4=
151+
github.com/itchyny/timefmt-go v0.1.1 h1:rLpnm9xxb39PEEVzO0n4IRp0q6/RmBc7Dy/rE4HrA0U=
152+
github.com/itchyny/timefmt-go v0.1.1/go.mod h1:0osSSCQSASBJMsIZnhAaF1C2fCBTJZXrnj37mG8/c+A=
146153
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
147154
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
148155
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
@@ -349,12 +356,15 @@ golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7w
349356
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
350357
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
351358
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
359+
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
352360
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8=
353361
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
354362
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
355363
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
356364
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 h1:opSr2sbRXk5X5/givKrrKj9HXxFpW2sdCiP8MJSKLQY=
357365
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
366+
golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78 h1:nVuTkr9L6Bq62qpUqKo/RnZCFfzDBL0bYo6w9OJUqZY=
367+
golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
358368
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
359369
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
360370
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -420,8 +430,8 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
420430
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
421431
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
422432
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
423-
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
424-
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
433+
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
434+
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
425435
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
426436
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
427437
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

pkg/cmd/api/api.go

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ type ApiOptions struct {
4343
Silent bool
4444
Template string
4545
CacheTTL time.Duration
46+
FilterOutput string
4647

4748
HttpClient func() (*http.Client, error)
4849
BaseRepo func() (ghrepo.Interface, error)
@@ -97,6 +98,11 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
9798
original query accepts an %[1]s$endCursor: String%[1]s variable and that it fetches the
9899
%[1]spageInfo{ hasNextPage, endCursor }%[1]s set of fields from a collection.
99100
101+
The %[1]s--jq%[1]s option accepts a query in jq syntax and will print only the resulting
102+
values that match the query. This is equivalent to piping the output to %[1]sjq -r%[1]s,
103+
but does not require the jq utility to be installed on the system. To learn more
104+
about the query syntax, see: https://stedolan.github.io/jq/manual/v1.6/
105+
100106
With %[1]s--template%[1]s, the provided Go template is rendered using the JSON data as input.
101107
For the syntax of Go templates, see: https://golang.org/pkg/text/template/
102108
@@ -124,6 +130,9 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
124130
# opt into GitHub API previews
125131
$ gh api --preview baptiste,nebula ...
126132
133+
# print only specific fields from the response
134+
$ gh api repos/:owner/:repo/issues --filter '.[].title'
135+
127136
# use a template for the output
128137
$ gh api repos/:owner/:repo/issues --template \
129138
'{{range .}}{{.title}} ({{.labels | pluck "name" | join ", " | color "yellow"}}){{"\n"}}{{end}}'
@@ -172,15 +181,29 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
172181

173182
if c.Flags().Changed("hostname") {
174183
if err := ghinstance.HostnameValidator(opts.Hostname); err != nil {
175-
return &cmdutil.FlagError{Err: fmt.Errorf("error parsing --hostname: %w", err)}
184+
return &cmdutil.FlagError{Err: fmt.Errorf("error parsing `--hostname`: %w", err)}
176185
}
177186
}
178187

179188
if opts.Paginate && !strings.EqualFold(opts.RequestMethod, "GET") && opts.RequestPath != "graphql" {
180-
return &cmdutil.FlagError{Err: errors.New(`the '--paginate' option is not supported for non-GET requests`)}
189+
return &cmdutil.FlagError{Err: errors.New("the `--paginate` option is not supported for non-GET requests")}
190+
}
191+
192+
if err := cmdutil.MutuallyExclusive(
193+
"the `--paginate` option is not supported with `--input`",
194+
opts.Paginate,
195+
opts.RequestInputFile != "",
196+
); err != nil {
197+
return err
181198
}
182-
if opts.Paginate && opts.RequestInputFile != "" {
183-
return &cmdutil.FlagError{Err: errors.New(`the '--paginate' option is not supported with '--input'`)}
199+
200+
if err := cmdutil.MutuallyExclusive(
201+
"only one of `--template`, `--jq`, or `--silent` may be used",
202+
opts.Silent,
203+
opts.FilterOutput != "",
204+
opts.Template != "",
205+
); err != nil {
206+
return err
184207
}
185208

186209
if runF != nil {
@@ -201,6 +224,7 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
201224
cmd.Flags().StringVar(&opts.RequestInputFile, "input", "", "The `file` to use as body for the HTTP request")
202225
cmd.Flags().BoolVar(&opts.Silent, "silent", false, "Do not print the response body")
203226
cmd.Flags().StringVarP(&opts.Template, "template", "t", "", "Format the response using a Go template")
227+
cmd.Flags().StringVarP(&opts.FilterOutput, "jq", "q", "", "Query to select values from the response using jq syntax")
204228
cmd.Flags().DurationVar(&opts.CacheTTL, "cache", 0, "Cache the response, e.g. \"3600s\", \"60m\", \"1h\"")
205229
return cmd
206230
}
@@ -332,7 +356,13 @@ func processResponse(resp *http.Response, opts *ApiOptions, headersOutputStream
332356
responseBody = io.TeeReader(responseBody, bodyCopy)
333357
}
334358

335-
if opts.Template != "" {
359+
if opts.FilterOutput != "" {
360+
// TODO: reuse parsed query across pagination invocations
361+
err = filterJSON(opts.IO.Out, responseBody, opts.FilterOutput)
362+
if err != nil {
363+
return
364+
}
365+
} else if opts.Template != "" {
336366
// TODO: reuse parsed template across pagination invocations
337367
err = executeTemplate(opts.IO.Out, responseBody, opts.Template, opts.IO.ColorEnabled())
338368
if err != nil {

pkg/cmd/api/api_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ func Test_NewCmdApi(t *testing.T) {
4848
Silent: false,
4949
CacheTTL: 0,
5050
Template: "",
51+
FilterOutput: "",
5152
},
5253
wantsErr: false,
5354
},
@@ -68,6 +69,7 @@ func Test_NewCmdApi(t *testing.T) {
6869
Silent: false,
6970
CacheTTL: 0,
7071
Template: "",
72+
FilterOutput: "",
7173
},
7274
wantsErr: false,
7375
},
@@ -88,6 +90,7 @@ func Test_NewCmdApi(t *testing.T) {
8890
Silent: false,
8991
CacheTTL: 0,
9092
Template: "",
93+
FilterOutput: "",
9194
},
9295
wantsErr: false,
9396
},
@@ -108,6 +111,7 @@ func Test_NewCmdApi(t *testing.T) {
108111
Silent: false,
109112
CacheTTL: 0,
110113
Template: "",
114+
FilterOutput: "",
111115
},
112116
wantsErr: false,
113117
},
@@ -128,6 +132,7 @@ func Test_NewCmdApi(t *testing.T) {
128132
Silent: false,
129133
CacheTTL: 0,
130134
Template: "",
135+
FilterOutput: "",
131136
},
132137
wantsErr: false,
133138
},
@@ -148,6 +153,7 @@ func Test_NewCmdApi(t *testing.T) {
148153
Silent: true,
149154
CacheTTL: 0,
150155
Template: "",
156+
FilterOutput: "",
151157
},
152158
wantsErr: false,
153159
},
@@ -173,6 +179,7 @@ func Test_NewCmdApi(t *testing.T) {
173179
Silent: false,
174180
CacheTTL: 0,
175181
Template: "",
182+
FilterOutput: "",
176183
},
177184
wantsErr: false,
178185
},
@@ -198,6 +205,7 @@ func Test_NewCmdApi(t *testing.T) {
198205
Silent: false,
199206
CacheTTL: 0,
200207
Template: "",
208+
FilterOutput: "",
201209
},
202210
wantsErr: false,
203211
},
@@ -223,6 +231,7 @@ func Test_NewCmdApi(t *testing.T) {
223231
Silent: false,
224232
CacheTTL: 0,
225233
Template: "",
234+
FilterOutput: "",
226235
},
227236
wantsErr: false,
228237
},
@@ -243,6 +252,7 @@ func Test_NewCmdApi(t *testing.T) {
243252
Silent: false,
244253
CacheTTL: time.Minute * 5,
245254
Template: "",
255+
FilterOutput: "",
246256
},
247257
wantsErr: false,
248258
},
@@ -263,9 +273,46 @@ func Test_NewCmdApi(t *testing.T) {
263273
Silent: false,
264274
CacheTTL: 0,
265275
Template: "hello {{.name}}",
276+
FilterOutput: "",
266277
},
267278
wantsErr: false,
268279
},
280+
{
281+
name: "with jq filter",
282+
cli: "user -q .name",
283+
wants: ApiOptions{
284+
Hostname: "",
285+
RequestMethod: "GET",
286+
RequestMethodPassed: false,
287+
RequestPath: "user",
288+
RequestInputFile: "",
289+
RawFields: []string(nil),
290+
MagicFields: []string(nil),
291+
RequestHeaders: []string(nil),
292+
ShowResponseHeaders: false,
293+
Paginate: false,
294+
Silent: false,
295+
CacheTTL: 0,
296+
Template: "",
297+
FilterOutput: ".name",
298+
},
299+
wantsErr: false,
300+
},
301+
{
302+
name: "--silent with --jq",
303+
cli: "user --silent -q .foo",
304+
wantsErr: true,
305+
},
306+
{
307+
name: "--silent with --template",
308+
cli: "user --silent -t '{{.foo}}'",
309+
wantsErr: true,
310+
},
311+
{
312+
name: "--jq with --template",
313+
cli: "user --jq .foo -t '{{.foo}}'",
314+
wantsErr: true,
315+
},
269316
}
270317
for _, tt := range tests {
271318
t.Run(tt.name, func(t *testing.T) {
@@ -301,6 +348,7 @@ func Test_NewCmdApi(t *testing.T) {
301348
assert.Equal(t, tt.wants.Silent, opts.Silent)
302349
assert.Equal(t, tt.wants.CacheTTL, opts.CacheTTL)
303350
assert.Equal(t, tt.wants.Template, opts.Template)
351+
assert.Equal(t, tt.wants.FilterOutput, opts.FilterOutput)
304352
})
305353
}
306354
}
@@ -440,6 +488,20 @@ func Test_apiRun(t *testing.T) {
440488
stdout: "not a cat",
441489
stderr: ``,
442490
},
491+
{
492+
name: "jq filter",
493+
options: ApiOptions{
494+
FilterOutput: `.[].name`,
495+
},
496+
httpResponse: &http.Response{
497+
StatusCode: 200,
498+
Body: ioutil.NopCloser(bytes.NewBufferString(`[{"name":"Mona"},{"name":"Hubot"}]`)),
499+
Header: http.Header{"Content-Type": []string{"application/json"}},
500+
},
501+
err: nil,
502+
stdout: "Mona\nHubot\n",
503+
stderr: ``,
504+
},
443505
}
444506

445507
for _, tt := range tests {

pkg/cmd/api/filter.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package api
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"io/ioutil"
8+
9+
"github.com/itchyny/gojq"
10+
)
11+
12+
func filterJSON(w io.Writer, input io.Reader, queryStr string) error {
13+
query, err := gojq.Parse(queryStr)
14+
if err != nil {
15+
return err
16+
}
17+
18+
jsonData, err := ioutil.ReadAll(input)
19+
if err != nil {
20+
return err
21+
}
22+
23+
var responseData interface{}
24+
err = json.Unmarshal(jsonData, &responseData)
25+
if err != nil {
26+
return err
27+
}
28+
29+
iter := query.Run(responseData)
30+
for {
31+
v, ok := iter.Next()
32+
if !ok {
33+
break
34+
}
35+
if err, isErr := v.(error); isErr {
36+
return err
37+
}
38+
if text, e := jsonScalarToString(v); e == nil {
39+
_, err := fmt.Fprintln(w, text)
40+
if err != nil {
41+
return err
42+
}
43+
} else {
44+
var jsonFragment []byte
45+
jsonFragment, err = json.Marshal(v)
46+
if err != nil {
47+
return err
48+
}
49+
_, err = w.Write(jsonFragment)
50+
if err != nil {
51+
return err
52+
}
53+
_, err = fmt.Fprint(w, "\n")
54+
if err != nil {
55+
return err
56+
}
57+
}
58+
}
59+
60+
return nil
61+
}

0 commit comments

Comments
 (0)
X Tutup