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()