diff --git a/pkg/cmd/api/api.go b/pkg/cmd/api/api.go index 2a80c464c..ea0734d21 100644 --- a/pkg/cmd/api/api.go +++ b/pkg/cmd/api/api.go @@ -387,7 +387,11 @@ func processResponse(resp *http.Response, opts *ApiOptions, bodyWriter, headersW if opts.FilterOutput != "" && serverError == "" { // TODO: reuse parsed query across pagination invocations - err = jq.Evaluate(responseBody, bodyWriter, opts.FilterOutput) + indent := "" + if opts.IO.IsStdoutTTY() { + indent = " " + } + err = jq.EvaluateFormatted(responseBody, bodyWriter, opts.FilterOutput, indent, opts.IO.ColorEnabled()) if err != nil { return } diff --git a/pkg/cmd/api/api_test.go b/pkg/cmd/api/api_test.go index d2fdc3c3c..ebbff6b6e 100644 --- a/pkg/cmd/api/api_test.go +++ b/pkg/cmd/api/api_test.go @@ -378,6 +378,7 @@ func Test_apiRun(t *testing.T) { err error stdout string stderr string + isatty bool }{ { name: "success", @@ -388,6 +389,7 @@ func Test_apiRun(t *testing.T) { err: nil, stdout: `bam!`, stderr: ``, + isatty: false, }, { name: "show response headers", @@ -404,6 +406,7 @@ func Test_apiRun(t *testing.T) { err: nil, stdout: "HTTP/1.1 200 Okey-dokey\nContent-Type: text/plain\r\n\r\nbody", stderr: ``, + isatty: false, }, { name: "success 204", @@ -414,6 +417,7 @@ func Test_apiRun(t *testing.T) { err: nil, stdout: ``, stderr: ``, + isatty: false, }, { name: "REST error", @@ -425,6 +429,7 @@ func Test_apiRun(t *testing.T) { err: cmdutil.SilentError, stdout: `{"message": "THIS IS FINE"}`, stderr: "gh: THIS IS FINE (HTTP 400)\n", + isatty: false, }, { name: "REST string errors", @@ -436,6 +441,7 @@ func Test_apiRun(t *testing.T) { err: cmdutil.SilentError, stdout: `{"errors": ["ALSO", "FINE"]}`, stderr: "gh: ALSO\nFINE\n", + isatty: false, }, { name: "GraphQL error", @@ -450,6 +456,7 @@ func Test_apiRun(t *testing.T) { err: cmdutil.SilentError, stdout: `{"errors": [{"message":"AGAIN"}, {"message":"FINE"}]}`, stderr: "gh: AGAIN\nFINE\n", + isatty: false, }, { name: "failure", @@ -460,6 +467,7 @@ func Test_apiRun(t *testing.T) { err: cmdutil.SilentError, stdout: `gateway timeout`, stderr: "gh: HTTP 502\n", + isatty: false, }, { name: "silent", @@ -473,6 +481,7 @@ func Test_apiRun(t *testing.T) { err: nil, stdout: ``, stderr: ``, + isatty: false, }, { name: "show response headers even when silent", @@ -490,6 +499,7 @@ func Test_apiRun(t *testing.T) { err: nil, stdout: "HTTP/1.1 200 Okey-dokey\nContent-Type: text/plain\r\n\r\n", stderr: ``, + isatty: false, }, { name: "output template", @@ -504,6 +514,7 @@ func Test_apiRun(t *testing.T) { err: nil, stdout: "not a cat", stderr: ``, + isatty: false, }, { name: "output template when REST error", @@ -518,6 +529,7 @@ func Test_apiRun(t *testing.T) { err: cmdutil.SilentError, stdout: `{"message": "THIS IS FINE"}`, stderr: "gh: THIS IS FINE (HTTP 400)\n", + isatty: false, }, { name: "jq filter", @@ -532,6 +544,7 @@ func Test_apiRun(t *testing.T) { err: nil, stdout: "Mona\nHubot\n", stderr: ``, + isatty: false, }, { name: "jq filter when REST error", @@ -546,12 +559,29 @@ func Test_apiRun(t *testing.T) { err: cmdutil.SilentError, stdout: `{"message": "THIS IS FINE"}`, stderr: "gh: THIS IS FINE (HTTP 400)\n", + isatty: false, + }, + { + name: "jq filter outputting JSON to a TTY", + options: ApiOptions{ + FilterOutput: `.`, + }, + httpResponse: &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(`[{"name":"Mona"},{"name":"Hubot"}]`)), + Header: http.Header{"Content-Type": []string{"application/json"}}, + }, + err: nil, + stdout: "[\n {\n \"name\": \"Mona\"\n },\n {\n \"name\": \"Hubot\"\n }\n]\n", + stderr: ``, + isatty: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ios, _, stdout, stderr := iostreams.Test() + ios.SetStdoutTTY(tt.isatty) tt.options.IO = ios tt.options.Config = func() (config.Config, error) { return config.NewBlankConfig(), nil } diff --git a/pkg/cmd/root/help_topic.go b/pkg/cmd/root/help_topic.go index 4a4d19a51..4df7d9e1a 100644 --- a/pkg/cmd/root/help_topic.go +++ b/pkg/cmd/root/help_topic.go @@ -111,8 +111,9 @@ var HelpTopics = map[string]map[string]string{ The %[1]s--jq%[1]s flag requires a string argument in jq query syntax, and will only print those JSON values which match the query. jq queries can be used to select elements from an array, fields from an object, create a new array, and more. The jq utility does not need - to be installed on the system to use this formatting directive. - To learn about jq query syntax, see: + to be installed on the system to use this formatting directive. When connected to a terminal, + the output is automatically pretty-printed. To learn about jq query syntax, see: + The %[1]s--template%[1]s flag requires a string argument in Go template syntax, and will only print those JSON values which match the query. @@ -174,7 +175,38 @@ var HelpTopics = map[string]map[string]string{ codercat cli-maintainer - + # --jq can be used to implement more complex filtering and output changes: + $ bin/gh issue list --json number,title,labels --jq \ + 'map(select((.labels | length) > 0)) # must have labels + | map(.labels = (.labels | map(.name))) # show only the label names + | .[:3] # select the first 3 results' + [ + { + "labels": [ + "enhancement", + "needs triage" + ], + "number": 123, + "title": "A helpful contribution" + }, + { + "labels": [ + "help wanted", + "docs", + "good first issue" + ], + "number": 125, + "title": "Improve the docs" + }, + { + "labels": [ + "enhancement", + ], + "number": 7221, + "title": "An exciting new feature" + } + ] + # using the --template flag with the hyperlink helper gh issue list --json title,url --template '{{range .}}{{hyperlink .url .title}}{{"\n"}}{{end}}' diff --git a/pkg/cmdutil/json_flags.go b/pkg/cmdutil/json_flags.go index d9bb89762..0aef38db4 100644 --- a/pkg/cmdutil/json_flags.go +++ b/pkg/cmdutil/json_flags.go @@ -138,7 +138,13 @@ func (e *exportFormat) Write(ios *iostreams.IOStreams, data interface{}) error { w := ios.Out if e.filter != "" { - return jq.Evaluate(&buf, w, e.filter) + indent := "" + if ios.IsStdoutTTY() { + indent = " " + } + if err := jq.EvaluateFormatted(&buf, w, e.filter, indent, ios.ColorEnabled()); err != nil { + return err + } } else if e.template != "" { t := template.New(w, ios.TerminalWidth(), ios.ColorEnabled()) if err := t.Parse(e.template); err != nil { diff --git a/pkg/cmdutil/json_flags_test.go b/pkg/cmdutil/json_flags_test.go index dd65647ea..975530df4 100644 --- a/pkg/cmdutil/json_flags_test.go +++ b/pkg/cmdutil/json_flags_test.go @@ -126,6 +126,7 @@ func Test_exportFormat_Write(t *testing.T) { args args wantW string wantErr bool + istty bool }{ { name: "regular JSON output", @@ -135,6 +136,7 @@ func Test_exportFormat_Write(t *testing.T) { }, wantW: "{\"name\":\"hubot\"}\n", wantErr: false, + istty: false, }, { name: "call ExportData", @@ -144,6 +146,7 @@ func Test_exportFormat_Write(t *testing.T) { }, wantW: "{\"field1\":\"item1:field1\",\"field2\":\"item1:field2\"}\n", wantErr: false, + istty: false, }, { name: "recursively call ExportData", @@ -156,6 +159,7 @@ func Test_exportFormat_Write(t *testing.T) { }, wantW: "{\"s1\":[{\"f1\":\"i1:f1\",\"f2\":\"i1:f2\"},{\"f1\":\"i2:f1\",\"f2\":\"i2:f2\"}],\"s2\":[{\"f1\":\"i3:f1\",\"f2\":\"i3:f2\"}]}\n", wantErr: false, + istty: false, }, { name: "with jq filter", @@ -165,6 +169,17 @@ func Test_exportFormat_Write(t *testing.T) { }, wantW: "hubot\n", wantErr: false, + istty: false, + }, + { + name: "with jq filter pretty printing", + exporter: exportFormat{filter: "."}, + args: args{ + data: map[string]string{"name": "hubot"}, + }, + wantW: "{\n \"name\": \"hubot\"\n}\n", + wantErr: false, + istty: true, }, { name: "with Go template", @@ -174,11 +189,13 @@ func Test_exportFormat_Write(t *testing.T) { }, wantW: "hubot", wantErr: false, + istty: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { io, _, w, _ := iostreams.Test() + io.SetStdoutTTY(tt.istty) if err := tt.exporter.Write(io, tt.args.data); (err != nil) != tt.wantErr { t.Errorf("exportFormat.Write() error = %v, wantErr %v", err, tt.wantErr) return