Skip to content

Commit

Permalink
add JSON output
Browse files Browse the repository at this point in the history
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
  • Loading branch information
wagoodman committed Mar 22, 2024
1 parent 26507e0 commit 250ca08
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 20 deletions.
134 changes: 124 additions & 10 deletions cmd/binny/cli/command/list.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package command

import (
"bytes"
"encoding/json"
"fmt"
"sort"
"strings"

"github.com/charmbracelet/lipgloss"
"github.com/itchyny/gojq"
"github.com/jedib0t/go-pretty/table"
"github.com/pkg/errors"
"github.com/spf13/cobra"
Expand All @@ -17,15 +21,20 @@ import (
)

type ListConfig struct {
Config string `json:"config" yaml:"config" mapstructure:"config"`
option.Check `json:"" yaml:",inline" mapstructure:",squash"`
option.Core `json:"" yaml:",inline" mapstructure:",squash"`
option.List `json:"" yaml:",inline" mapstructure:",squash"`
Config string `json:"config" yaml:"config" mapstructure:"config"`
option.Check `json:"" yaml:",inline" mapstructure:",squash"`
option.Core `json:"" yaml:",inline" mapstructure:",squash"`
option.List `json:"" yaml:",inline" mapstructure:",squash"`
option.Format `json:"" yaml:",inline" mapstructure:",squash"`
}

func List(app clio.Application) *cobra.Command {
cfg := &ListConfig{
Core: option.DefaultCore(),
Format: option.Format{
Output: "table",
AllowableFormats: []string{"table", "json"},
},
}

return app.SetupCommand(&cobra.Command{
Expand All @@ -34,8 +43,12 @@ func List(app clio.Application) *cobra.Command {
Aliases: []string{
"ls",
},
Args: cobra.NoArgs,
Args: cobra.ArbitraryArgs,
PreRunE: func(cmd *cobra.Command, args []string) error {
if cfg.Format.JQCommand != "" && cfg.Format.Output != "json" {
return fmt.Errorf("--jq can only be used when --output format is 'json'")
}
cfg.IncludeFilter = args
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
Expand All @@ -48,10 +61,10 @@ type toolStatus struct {
Name string `json:"name"`
WantedVersion string `json:"wantedVersion"` // this is the version the user asked for
ResolvedVersion string `json:"resolvedVersion"` // if the user asks for a non-specific version (e.g. "latest") then this is what that would resolve to at this point in time
InstalledVersion string `json:"installedVersion"` // the actual version that is installed, which could vary from the user wanted or resolved values
Constraint string `json:"constraint"` // the version constraint the user asked for and used during version resolution
IsInstalled bool `json:"isInstalled"` // is the tool installed at the desired version (says nothing about it being valid, only present)
HashIsValid bool `json:"hashIsValid"` // is the installed tool have the correct xxh64 hash?
InstalledVersion string `json:"installedVersion"` // the actual version that is installed, which could vary from the user wanted or resolved values
Error error `json:"error,omitempty"` // if there was an error getting the status for this tool, it will be here
}

Expand All @@ -67,11 +80,112 @@ func runList(cmdCfg ListConfig) error {
// look for items in the store root that cannot be accounted for
// TODO

statuses := filterStatus(allStatuses, cmdCfg.List.IncludeFilter)

if cmdCfg.Format.Output == "json" {
return presentJSON(statuses, cmdCfg.List.Updates, cmdCfg.Format.JQCommand)
}

if cmdCfg.List.Updates {
return presentUpdates(allStatuses)
return presentUpdatesTable(statuses)
}

return presentListTable(statuses)
}

func filterStatus(status []toolStatus, includeFilter []string) []toolStatus {
if len(includeFilter) == 0 {
return status
}

filtered := make([]toolStatus, 0)
for _, s := range status {
for _, f := range includeFilter {
if s.Name == f {
filtered = append(filtered, s)
}
}
}

return filtered
}

func presentJSON(statuses []toolStatus, updatesOnly bool, jqCommand string) error {

Check failure on line 113 in cmd/binny/cli/command/list.go

View workflow job for this annotation

GitHub Actions / Validations

cognitive complexity 34 of func `presentJSON` is high (> 30) (gocognit)
if updatesOnly {
var updates []toolStatus
for _, status := range statuses {
if status.Error != nil {
status.InstalledVersion = ""
status.ResolvedVersion = ""
status.Constraint = ""
status.HashIsValid = false
updates = append(updates, status)
continue
}

if status.WantedVersion == "?" {
continue
}

if status.InstalledVersion != status.ResolvedVersion {
updates = append(updates, status)
continue
}

if !status.HashIsValid {
updates = append(updates, status)
}
}
statuses = updates
}

doc := make(map[string]any)
doc["tools"] = statuses

buf := bytes.Buffer{}
enc := json.NewEncoder(&buf)
enc.SetIndent("", " ")
if err := enc.Encode(doc); err != nil {
return fmt.Errorf("unable to encode JSON: %w", err)
}

return presentList(allStatuses)
if jqCommand != "" {
query, err := gojq.Parse(jqCommand)
if err != nil {
return fmt.Errorf("unable to parse JQ command: %w", err)
}

decodeDoc := make(map[string]any)
if err := json.Unmarshal(buf.Bytes(), &decodeDoc); err != nil {
return fmt.Errorf("unable to decode JSON: %w", err)
}

buf.Reset()
iter := query.Run(decodeDoc)
for {
v, ok := iter.Next()
if !ok {
break
}
if err, ok := v.(error); ok {
return fmt.Errorf("error executing JQ command: %w", err)
}

if err := enc.Encode(v); err != nil {
return fmt.Errorf("unable to encode JSON: %w", err)
}
}
}

report := buf.String()

// default to raw output for simple JSON output
if strings.HasPrefix(report, "\"") && strings.HasSuffix(report, "\"\n") {
report = strings.ReplaceAll(report, "\"", "")
}

bus.Report(report)
return nil
}

func getAllStatuses(cmdCfg ListConfig, store *binny.Store) []toolStatus {
Expand Down Expand Up @@ -208,7 +322,7 @@ func removeEntry(entries []binny.StoreEntry, entry *binny.StoreEntry) []binny.St
return entries
}

func presentUpdates(statuses []toolStatus) error {
func presentUpdatesTable(statuses []toolStatus) error {
t := table.NewWriter()
t.SetStyle(table.StyleLight)
t.Style().Options.DrawBorder = false
Expand Down Expand Up @@ -286,7 +400,7 @@ func getToolUpdatesRow(item toolStatus) table.Row {
return row
}

func presentList(statuses []toolStatus) error {
func presentListTable(statuses []toolStatus) error {
if len(statuses) == 0 {
bus.Report("no tools configured or installed")
return nil
Expand Down
28 changes: 28 additions & 0 deletions cmd/binny/cli/option/format.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package option

import (
"fmt"

"github.com/anchore/fangs"
)

var _ fangs.FlagAdder = (*Format)(nil)

type Format struct {
Output string `yaml:"output" json:"output" mapstructure:"output"`
AllowableFormats []string `yaml:"-" json:"-" mapstructure:"-"`
JQCommand string `yaml:"jqCommand" json:"jqCommand" mapstructure:"jqCommand"`
}

func (o *Format) AddFlags(flags fangs.FlagSet) {
flags.StringVarP(
&o.Output,
"output", "o",
fmt.Sprintf("output format to report results in (allowable values: %s)", o.AllowableFormats),
)
flags.StringVarP(
&o.JQCommand,
"jq", "",
"JQ command to apply to the JSON output",
)
}
3 changes: 2 additions & 1 deletion cmd/binny/cli/option/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ package option
import "github.com/anchore/clio"

type List struct {
Updates bool `json:"updates" yaml:"updates" mapstructure:"updates"`
Updates bool `json:"updates" yaml:"updates" mapstructure:"updates"`
IncludeFilter []string `json:"includeFilter" yaml:"includeFilter" mapstructure:"includeFilter"`
}

func (o *List) AddFlags(flags clio.FlagSet) {
Expand Down
10 changes: 6 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/OneOfOne/xxhash v1.2.8
github.com/anchore/bubbly v0.0.0-20230919123500-747f4abea05f
github.com/anchore/clio v0.0.0-20230823172630-c42d666061af
github.com/anchore/fangs v0.0.0-20230818131516-2186b10924fe
github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a
github.com/chainguard-dev/yam v0.0.0-20230904174023-8d3c53b7e9d7
github.com/charmbracelet/bubbles v0.16.1
Expand All @@ -21,6 +22,7 @@ require (
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/google/yamlfmt v0.9.1-0.20230607021126-908b19015fc4
github.com/hashicorp/go-multierror v1.1.1
github.com/itchyny/gojq v0.12.14
github.com/jedib0t/go-pretty v4.3.0+incompatible
github.com/mholt/archiver/v3 v3.5.1
github.com/mitchellh/hashstructure/v2 v2.0.2
Expand All @@ -47,7 +49,6 @@ require (
github.com/RageCage64/multilinediff v0.2.0 // indirect
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
github.com/adrg/xdg v0.4.0 // indirect
github.com/anchore/fangs v0.0.0-20230818131516-2186b10924fe // indirect
github.com/andybalholm/brotli v1.0.4 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
Expand Down Expand Up @@ -80,6 +81,7 @@ require (
github.com/iancoleman/strcase v0.3.0 // indirect
github.com/imdario/mergo v0.3.15 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/itchyny/timefmt-go v0.1.5 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.16.5 // indirect
Expand All @@ -89,9 +91,9 @@ require (
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
Expand All @@ -108,7 +110,7 @@ require (
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pkg/profile v1.7.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/sergi/go-diff v1.3.1 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
Expand Down
15 changes: 10 additions & 5 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,10 @@ github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/itchyny/gojq v0.12.14 h1:6k8vVtsrhQSYgSGg827AD+PVVaB1NLXEdX+dda2oZCc=
github.com/itchyny/gojq v0.12.14/go.mod h1:y1G7oO7XkcR1LPZO59KyoCRy08T3j9vDYRV0GgYSS+s=
github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE=
github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
Expand Down Expand Up @@ -295,13 +299,13 @@ github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3v
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw=
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
Expand Down Expand Up @@ -353,8 +357,9 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
Expand Down

0 comments on commit 250ca08

Please sign in to comment.