X Tutup
Skip to content

Commit 03baeb2

Browse files
committed
Add documentation and tests for api --filter
1 parent 9f4eb55 commit 03baeb2

File tree

3 files changed

+156
-31
lines changed

3 files changed

+156
-31
lines changed

pkg/cmd/api/api.go

Lines changed: 10 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import (
2424
"github.com/cli/cli/pkg/cmdutil"
2525
"github.com/cli/cli/pkg/iostreams"
2626
"github.com/cli/cli/pkg/jsoncolor"
27-
"github.com/itchyny/gojq"
2827
"github.com/spf13/cobra"
2928
)
3029

@@ -99,6 +98,10 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
9998
original query accepts an %[1]s$endCursor: String%[1]s variable and that it fetches the
10099
%[1]spageInfo{ hasNextPage, endCursor }%[1]s set of fields from a collection.
101100
101+
The %[1]s--filter%[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+
To learn more about the query syntax, see: https://stedolan.github.io/jq/manual/v1.6/
104+
102105
With %[1]s--template%[1]s, the provided Go template is rendered using the JSON data as input.
103106
For the syntax of Go templates, see: https://golang.org/pkg/text/template/
104107
@@ -123,6 +126,9 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
123126
# set a custom HTTP header
124127
$ gh api -H 'Accept: application/vnd.github.XYZ-preview+json' ...
125128
129+
# print only specific fields from the response
130+
$ gh api repos/:owner/:repo/issues --filter '.[].title'
131+
126132
# use a template for the output
127133
$ gh api repos/:owner/:repo/issues --template \
128134
'{{range .}}{{.title}} ({{.labels | pluck "name" | join ", " | color "yellow"}}){{"\n"}}{{end}}'
@@ -199,7 +205,7 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
199205
cmd.Flags().StringVar(&opts.RequestInputFile, "input", "", "The `file` to use as body for the HTTP request")
200206
cmd.Flags().BoolVar(&opts.Silent, "silent", false, "Do not print the response body")
201207
cmd.Flags().StringVarP(&opts.Template, "template", "t", "", "Format the response using a Go template")
202-
cmd.Flags().StringVar(&opts.FilterOutput, "filter", "", "Filter the response using jq syntax")
208+
cmd.Flags().StringVar(&opts.FilterOutput, "filter", "", "Filter fields from the response using jq syntax")
203209
cmd.Flags().DurationVar(&opts.CacheTTL, "cache", 0, "Cache the response, e.g. \"3600s\", \"60m\", \"1h\"")
204210
return cmd
205211
}
@@ -328,38 +334,11 @@ func processResponse(resp *http.Response, opts *ApiOptions, headersOutputStream
328334
}
329335

330336
if opts.FilterOutput != "" {
331-
var query *gojq.Query
332-
query, err = gojq.Parse(opts.FilterOutput)
333-
if err != nil {
334-
return
335-
}
336-
337-
var jsonData []byte
338-
jsonData, err = ioutil.ReadAll(responseBody)
337+
// TODO: reuse parsed query across pagination invocations
338+
err = filterJSON(opts.IO.Out, responseBody, opts.FilterOutput)
339339
if err != nil {
340340
return
341341
}
342-
343-
var responseData interface{}
344-
err = json.Unmarshal(jsonData, &responseData)
345-
if err != nil {
346-
return
347-
}
348-
349-
iter := query.Run(responseData)
350-
for {
351-
var v interface{}
352-
var ok bool
353-
v, ok = iter.Next()
354-
if !ok {
355-
break
356-
}
357-
err, ok = v.(error)
358-
if ok {
359-
return
360-
}
361-
fmt.Fprintf(opts.IO.Out, "%v\n", v)
362-
}
363342
} else if opts.Template != "" {
364343
// TODO: reuse parsed template across pagination invocations
365344
var t *template.Template

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+
}

pkg/cmd/api/filter_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package api
2+
3+
import (
4+
"bytes"
5+
"io"
6+
"strings"
7+
"testing"
8+
9+
"github.com/MakeNowJust/heredoc"
10+
)
11+
12+
func Test_filterJSON(t *testing.T) {
13+
type args struct {
14+
json io.Reader
15+
query string
16+
}
17+
tests := []struct {
18+
name string
19+
args args
20+
wantW string
21+
wantErr bool
22+
}{
23+
{
24+
name: "simple",
25+
args: args{
26+
json: strings.NewReader(`{"name":"Mona", "arms":8}`),
27+
query: `.name`,
28+
},
29+
wantW: "Mona\n",
30+
},
31+
{
32+
name: "multiple queries",
33+
args: args{
34+
json: strings.NewReader(`{"name":"Mona", "arms":8}`),
35+
query: `.name,.arms`,
36+
},
37+
wantW: "Mona\n8\n",
38+
},
39+
{
40+
name: "object as JSON",
41+
args: args{
42+
json: strings.NewReader(`{"user":{"login":"monalisa"}}`),
43+
query: `.user`,
44+
},
45+
wantW: "{\"login\":\"monalisa\"}\n",
46+
},
47+
{
48+
name: "complex",
49+
args: args{
50+
json: strings.NewReader(heredoc.Doc(`[
51+
{
52+
"title": "First title",
53+
"labels": [{"name":"bug"}, {"name":"help wanted"}]
54+
},
55+
{
56+
"title": "Second but not last",
57+
"labels": []
58+
},
59+
{
60+
"title": "Alas, tis' the end",
61+
"labels": [{}, {"name":"feature"}]
62+
}
63+
]`)),
64+
query: `.[] | [.title,(.labels | map(.name) | join(","))] | @tsv`,
65+
},
66+
wantW: heredoc.Doc(`
67+
First title bug,help wanted
68+
Second but not last
69+
Alas, tis' the end ,feature
70+
`),
71+
},
72+
}
73+
for _, tt := range tests {
74+
t.Run(tt.name, func(t *testing.T) {
75+
w := &bytes.Buffer{}
76+
if err := filterJSON(w, tt.args.json, tt.args.query); (err != nil) != tt.wantErr {
77+
t.Errorf("filterJSON() error = %v, wantErr %v", err, tt.wantErr)
78+
return
79+
}
80+
if gotW := w.String(); gotW != tt.wantW {
81+
t.Errorf("filterJSON() = %q, want %q", gotW, tt.wantW)
82+
}
83+
})
84+
}
85+
}

0 commit comments

Comments
 (0)
X Tutup