Pretty-print JSON results of jq filtering (#7236)
When connected to a TTY, tell the jq formatter to indent the output, and enable colorization of the output if the terminal supports it. Co-authored-by: Mislav Marohnić <mislav@github.com>
This commit is contained in:
parent
6a6df0d39b
commit
530002ee7a
5 changed files with 94 additions and 5 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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: <https://stedolan.github.io/jq/manual/v1.6/>
|
||||
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:
|
||||
<https://stedolan.github.io/jq/manual/v1.6/>
|
||||
|
||||
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}}'
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue