github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/pkg/cmdutil/json_flags.go (about) 1 package cmdutil 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "reflect" 10 "sort" 11 "strings" 12 13 "github.com/cli/cli/pkg/export" 14 "github.com/cli/cli/pkg/iostreams" 15 "github.com/cli/cli/pkg/jsoncolor" 16 "github.com/cli/cli/pkg/set" 17 "github.com/spf13/cobra" 18 "github.com/spf13/pflag" 19 ) 20 21 type JSONFlagError struct { 22 error 23 } 24 25 func AddJSONFlags(cmd *cobra.Command, exportTarget *Exporter, fields []string) { 26 f := cmd.Flags() 27 f.StringSlice("json", nil, "Output JSON with the specified `fields`") 28 f.StringP("jq", "q", "", "Filter JSON output using a jq `expression`") 29 f.StringP("template", "t", "", "Format JSON output using a Go template") 30 31 _ = cmd.RegisterFlagCompletionFunc("json", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 32 var results []string 33 var prefix string 34 if idx := strings.LastIndexByte(toComplete, ','); idx >= 0 { 35 prefix = toComplete[:idx+1] 36 toComplete = toComplete[idx+1:] 37 } 38 toComplete = strings.ToLower(toComplete) 39 for _, f := range fields { 40 if strings.HasPrefix(strings.ToLower(f), toComplete) { 41 results = append(results, prefix+f) 42 } 43 } 44 sort.Strings(results) 45 return results, cobra.ShellCompDirectiveNoSpace 46 }) 47 48 oldPreRun := cmd.PreRunE 49 cmd.PreRunE = func(c *cobra.Command, args []string) error { 50 if oldPreRun != nil { 51 if err := oldPreRun(c, args); err != nil { 52 return err 53 } 54 } 55 if export, err := checkJSONFlags(c); err == nil { 56 if export == nil { 57 *exportTarget = nil 58 } else { 59 allowedFields := set.NewStringSet() 60 allowedFields.AddValues(fields) 61 for _, f := range export.fields { 62 if !allowedFields.Contains(f) { 63 sort.Strings(fields) 64 return JSONFlagError{fmt.Errorf("Unknown JSON field: %q\nAvailable fields:\n %s", f, strings.Join(fields, "\n "))} 65 } 66 } 67 *exportTarget = export 68 } 69 } else { 70 return err 71 } 72 return nil 73 } 74 75 cmd.SetFlagErrorFunc(func(c *cobra.Command, e error) error { 76 if e.Error() == "flag needs an argument: --json" { 77 sort.Strings(fields) 78 return JSONFlagError{fmt.Errorf("Specify one or more comma-separated fields for `--json`:\n %s", strings.Join(fields, "\n "))} 79 } 80 return c.Parent().FlagErrorFunc()(c, e) 81 }) 82 } 83 84 func checkJSONFlags(cmd *cobra.Command) (*exportFormat, error) { 85 f := cmd.Flags() 86 jsonFlag := f.Lookup("json") 87 jqFlag := f.Lookup("jq") 88 tplFlag := f.Lookup("template") 89 webFlag := f.Lookup("web") 90 91 if jsonFlag.Changed { 92 if webFlag != nil && webFlag.Changed { 93 return nil, errors.New("cannot use `--web` with `--json`") 94 } 95 jv := jsonFlag.Value.(pflag.SliceValue) 96 return &exportFormat{ 97 fields: jv.GetSlice(), 98 filter: jqFlag.Value.String(), 99 template: tplFlag.Value.String(), 100 }, nil 101 } else if jqFlag.Changed { 102 return nil, errors.New("cannot use `--jq` without specifying `--json`") 103 } else if tplFlag.Changed { 104 return nil, errors.New("cannot use `--template` without specifying `--json`") 105 } 106 return nil, nil 107 } 108 109 type Exporter interface { 110 Fields() []string 111 Write(io *iostreams.IOStreams, data interface{}) error 112 } 113 114 type exportFormat struct { 115 fields []string 116 filter string 117 template string 118 } 119 120 func (e *exportFormat) Fields() []string { 121 return e.fields 122 } 123 124 // Write serializes data into JSON output written to w. If the object passed as data implements exportable, 125 // or if data is a map or slice of exportable object, ExportData() will be called on each object to obtain 126 // raw data for serialization. 127 func (e *exportFormat) Write(ios *iostreams.IOStreams, data interface{}) error { 128 buf := bytes.Buffer{} 129 encoder := json.NewEncoder(&buf) 130 encoder.SetEscapeHTML(false) 131 if err := encoder.Encode(e.exportData(reflect.ValueOf(data))); err != nil { 132 return err 133 } 134 135 w := ios.Out 136 if e.filter != "" { 137 return export.FilterJSON(w, &buf, e.filter) 138 } else if e.template != "" { 139 return export.ExecuteTemplate(ios, &buf, e.template) 140 } else if ios.ColorEnabled() { 141 return jsoncolor.Write(w, &buf, " ") 142 } 143 144 _, err := io.Copy(w, &buf) 145 return err 146 } 147 148 func (e *exportFormat) exportData(v reflect.Value) interface{} { 149 switch v.Kind() { 150 case reflect.Ptr, reflect.Interface: 151 if !v.IsNil() { 152 return e.exportData(v.Elem()) 153 } 154 case reflect.Slice: 155 a := make([]interface{}, v.Len()) 156 for i := 0; i < v.Len(); i++ { 157 a[i] = e.exportData(v.Index(i)) 158 } 159 return a 160 case reflect.Map: 161 t := reflect.MapOf(v.Type().Key(), emptyInterfaceType) 162 m := reflect.MakeMapWithSize(t, v.Len()) 163 iter := v.MapRange() 164 for iter.Next() { 165 ve := reflect.ValueOf(e.exportData(iter.Value())) 166 m.SetMapIndex(iter.Key(), ve) 167 } 168 return m.Interface() 169 case reflect.Struct: 170 if v.CanAddr() && reflect.PtrTo(v.Type()).Implements(exportableType) { 171 ve := v.Addr().Interface().(exportable) 172 return ve.ExportData(e.fields) 173 } else if v.Type().Implements(exportableType) { 174 ve := v.Interface().(exportable) 175 return ve.ExportData(e.fields) 176 } 177 } 178 return v.Interface() 179 } 180 181 type exportable interface { 182 ExportData([]string) *map[string]interface{} 183 } 184 185 var exportableType = reflect.TypeOf((*exportable)(nil)).Elem() 186 var sliceOfEmptyInterface []interface{} 187 var emptyInterfaceType = reflect.TypeOf(sliceOfEmptyInterface).Elem()