From 517cfc236573a7ff872dae9e010a4929d7374e9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Mon, 22 Feb 2021 16:15:02 +0100 Subject: [PATCH 1/6] Add `api --format` flag for specifying an output template With the `--format` flag, the value of the flag is parsed as a Go template which is then evaluated against parsed response data. https://golang.org/pkg/text/template --- pkg/cmd/api/api.go | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/api/api.go b/pkg/cmd/api/api.go index 025225825..79f5432db 100644 --- a/pkg/cmd/api/api.go +++ b/pkg/cmd/api/api.go @@ -14,6 +14,7 @@ import ( "strconv" "strings" "syscall" + "text/template" "time" "github.com/MakeNowJust/heredoc" @@ -40,6 +41,7 @@ type ApiOptions struct { ShowResponseHeaders bool Paginate bool Silent bool + Template string CacheTTL time.Duration HttpClient func() (*http.Client, error) @@ -189,6 +191,7 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command cmd.Flags().BoolVar(&opts.Paginate, "paginate", false, "Make additional HTTP requests to fetch all pages of results") 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, "format", "t", "", "Format the response using a Go template") cmd.Flags().StringVar(&cacheDuration, "cache", "", "Cache the response for the specified duration") return cmd } @@ -316,7 +319,27 @@ func processResponse(resp *http.Response, opts *ApiOptions, headersOutputStream responseBody = io.TeeReader(responseBody, bodyCopy) } - if isJSON && opts.IO.ColorEnabled() { + if opts.Template != "" { + // TODO: reuse parsed template across pagination invocations + t := template.Must(template.New("").Parse(opts.Template)) + + var jsonData []byte + jsonData, err = ioutil.ReadAll(responseBody) + if err != nil { + return + } + + var m interface{} + err = json.Unmarshal(jsonData, &m) + if err != nil { + return + } + + err = t.Execute(opts.IO.Out, m) + if err != nil { + return + } + } else if isJSON && opts.IO.ColorEnabled() { err = jsoncolor.Write(opts.IO.Out, responseBody, " ") } else { _, err = io.Copy(opts.IO.Out, responseBody) From fd82d621d5fa24b8de18d78d05ab93a5b978d71b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Mon, 22 Feb 2021 16:54:27 +0100 Subject: [PATCH 2/6] Add `color` function to api templates --- pkg/cmd/api/api.go | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/api/api.go b/pkg/cmd/api/api.go index 79f5432db..53e6188bc 100644 --- a/pkg/cmd/api/api.go +++ b/pkg/cmd/api/api.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "io/ioutil" + "math" "net/http" "os" "regexp" @@ -24,6 +25,7 @@ import ( "github.com/cli/cli/pkg/cmdutil" "github.com/cli/cli/pkg/iostreams" "github.com/cli/cli/pkg/jsoncolor" + "github.com/mgutz/ansi" "github.com/spf13/cobra" ) @@ -320,8 +322,31 @@ func processResponse(resp *http.Response, opts *ApiOptions, headersOutputStream } if opts.Template != "" { + templateFuncs := map[string]interface{}{ + "color": func(colorName string, input interface{}) (string, error) { + var text string + switch tt := input.(type) { + case string: + text = tt + case float64: + if math.Trunc(tt) == tt { + text = strconv.FormatFloat(tt, 'f', 0, 64) + } else { + text = strconv.FormatFloat(tt, 'f', 2, 64) + } + case nil: + text = "" + case bool: + text = fmt.Sprintf("%v", tt) + default: + return "", fmt.Errorf("cannot convert type to string: %v", tt) + } + return ansi.Color(text, colorName), nil + }, + } + // TODO: reuse parsed template across pagination invocations - t := template.Must(template.New("").Parse(opts.Template)) + t := template.Must(template.New("").Funcs(templateFuncs).Parse(opts.Template)) var jsonData []byte jsonData, err = ioutil.ReadAll(responseBody) From bf97c6e273ff419f22b4da0e9ff9a22bfccc0d12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Tue, 2 Mar 2021 20:07:04 +0100 Subject: [PATCH 3/6] Add template functions, documentation, tests --- pkg/cmd/api/api.go | 48 ++++++++---------- pkg/cmd/api/api_test.go | 39 +++++++++++++++ pkg/cmd/api/template.go | 96 ++++++++++++++++++++++++++++++++++++ pkg/cmd/api/template_test.go | 60 ++++++++++++++++++++++ 4 files changed, 216 insertions(+), 27 deletions(-) create mode 100644 pkg/cmd/api/template.go create mode 100644 pkg/cmd/api/template_test.go diff --git a/pkg/cmd/api/api.go b/pkg/cmd/api/api.go index ac49d715b..7d7f23a92 100644 --- a/pkg/cmd/api/api.go +++ b/pkg/cmd/api/api.go @@ -7,7 +7,6 @@ import ( "fmt" "io" "io/ioutil" - "math" "net/http" "os" "regexp" @@ -25,7 +24,6 @@ import ( "github.com/cli/cli/pkg/cmdutil" "github.com/cli/cli/pkg/iostreams" "github.com/cli/cli/pkg/jsoncolor" - "github.com/mgutz/ansi" "github.com/spf13/cobra" ) @@ -98,6 +96,17 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command there are no more pages of results. For GraphQL requests, this requires that the 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. + + 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/ + + The following functions are available in templates: + - %[1]scolor