diff --git a/pkg/cmd/api/api.go b/pkg/cmd/api/api.go index 5dd9be449..81c0245ae 100644 --- a/pkg/cmd/api/api.go +++ b/pkg/cmd/api/api.go @@ -24,7 +24,6 @@ import ( "github.com/cli/cli/pkg/cmdutil" "github.com/cli/cli/pkg/iostreams" "github.com/cli/cli/pkg/jsoncolor" - "github.com/itchyny/gojq" "github.com/spf13/cobra" ) @@ -99,6 +98,10 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command original query accepts an %[1]s$endCursor: String%[1]s variable and that it fetches the %[1]spageInfo{ hasNextPage, endCursor }%[1]s set of fields from a collection. + The %[1]s--filter%[1]s option accepts a query in jq syntax and will print only the resulting + values that match the query. This is equivalent to piping the output to %[1]sjq -r%[1]s. + To learn more about the query syntax, see: https://stedolan.github.io/jq/manual/v1.6/ + With %[1]s--template%[1]s, the provided Go template is rendered using the JSON data as input. For the syntax of Go templates, see: https://golang.org/pkg/text/template/ @@ -123,6 +126,9 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command # set a custom HTTP header $ gh api -H 'Accept: application/vnd.github.XYZ-preview+json' ... + # print only specific fields from the response + $ gh api repos/:owner/:repo/issues --filter '.[].title' + # use a template for the output $ gh api repos/:owner/:repo/issues --template \ '{{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 cmd.Flags().StringVar(&opts.RequestInputFile, "input", "", "The `file` to use as body for the HTTP request") cmd.Flags().BoolVar(&opts.Silent, "silent", false, "Do not print the response body") cmd.Flags().StringVarP(&opts.Template, "template", "t", "", "Format the response using a Go template") - cmd.Flags().StringVar(&opts.FilterOutput, "filter", "", "Filter the response using jq syntax") + cmd.Flags().StringVar(&opts.FilterOutput, "filter", "", "Filter fields from the response using jq syntax") cmd.Flags().DurationVar(&opts.CacheTTL, "cache", 0, "Cache the response, e.g. \"3600s\", \"60m\", \"1h\"") return cmd } @@ -328,38 +334,11 @@ func processResponse(resp *http.Response, opts *ApiOptions, headersOutputStream } if opts.FilterOutput != "" { - var query *gojq.Query - query, err = gojq.Parse(opts.FilterOutput) + // TODO: reuse parsed query across pagination invocations + err = filterJSON(opts.IO.Out, responseBody, opts.FilterOutput) if err != nil { return } - - var jsonData []byte - jsonData, err = ioutil.ReadAll(responseBody) - if err != nil { - return - } - - var responseData interface{} - err = json.Unmarshal(jsonData, &responseData) - if err != nil { - return - } - - iter := query.Run(responseData) - for { - var v interface{} - var ok bool - v, ok = iter.Next() - if !ok { - break - } - err, ok = v.(error) - if ok { - return - } - fmt.Fprintf(opts.IO.Out, "%v\n", v) - } } else if opts.Template != "" { // TODO: reuse parsed template across pagination invocations var t *template.Template diff --git a/pkg/cmd/api/filter.go b/pkg/cmd/api/filter.go new file mode 100644 index 000000000..4872ed353 --- /dev/null +++ b/pkg/cmd/api/filter.go @@ -0,0 +1,61 @@ +package api + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + + "github.com/itchyny/gojq" +) + +func filterJSON(w io.Writer, input io.Reader, queryStr string) error { + query, err := gojq.Parse(queryStr) + if err != nil { + return err + } + + jsonData, err := ioutil.ReadAll(input) + if err != nil { + return err + } + + var responseData interface{} + err = json.Unmarshal(jsonData, &responseData) + if err != nil { + return err + } + + iter := query.Run(responseData) + for { + v, ok := iter.Next() + if !ok { + break + } + if err, isErr := v.(error); isErr { + return err + } + if text, e := jsonScalarToString(v); e == nil { + _, err := fmt.Fprintln(w, text) + if err != nil { + return err + } + } else { + var jsonFragment []byte + jsonFragment, err = json.Marshal(v) + if err != nil { + return err + } + _, err = w.Write(jsonFragment) + if err != nil { + return err + } + _, err = fmt.Fprint(w, "\n") + if err != nil { + return err + } + } + } + + return nil +} diff --git a/pkg/cmd/api/filter_test.go b/pkg/cmd/api/filter_test.go new file mode 100644 index 000000000..93dc4fbe8 --- /dev/null +++ b/pkg/cmd/api/filter_test.go @@ -0,0 +1,85 @@ +package api + +import ( + "bytes" + "io" + "strings" + "testing" + + "github.com/MakeNowJust/heredoc" +) + +func Test_filterJSON(t *testing.T) { + type args struct { + json io.Reader + query string + } + tests := []struct { + name string + args args + wantW string + wantErr bool + }{ + { + name: "simple", + args: args{ + json: strings.NewReader(`{"name":"Mona", "arms":8}`), + query: `.name`, + }, + wantW: "Mona\n", + }, + { + name: "multiple queries", + args: args{ + json: strings.NewReader(`{"name":"Mona", "arms":8}`), + query: `.name,.arms`, + }, + wantW: "Mona\n8\n", + }, + { + name: "object as JSON", + args: args{ + json: strings.NewReader(`{"user":{"login":"monalisa"}}`), + query: `.user`, + }, + wantW: "{\"login\":\"monalisa\"}\n", + }, + { + name: "complex", + args: args{ + json: strings.NewReader(heredoc.Doc(`[ + { + "title": "First title", + "labels": [{"name":"bug"}, {"name":"help wanted"}] + }, + { + "title": "Second but not last", + "labels": [] + }, + { + "title": "Alas, tis' the end", + "labels": [{}, {"name":"feature"}] + } + ]`)), + query: `.[] | [.title,(.labels | map(.name) | join(","))] | @tsv`, + }, + wantW: heredoc.Doc(` + First title bug,help wanted + Second but not last + Alas, tis' the end ,feature + `), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := &bytes.Buffer{} + if err := filterJSON(w, tt.args.json, tt.args.query); (err != nil) != tt.wantErr { + t.Errorf("filterJSON() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotW := w.String(); gotW != tt.wantW { + t.Errorf("filterJSON() = %q, want %q", gotW, tt.wantW) + } + }) + } +}