shell completion --format: work with nil structs

AutocompleteFormat() takes the format struct as argument. Often the structs
are deeply nested and contain other structs. Up until now if there was a
pointer to a struct the logic was not able to get the field names from
that, simply because the pointer was nil. However it is possible to
create a new initialized type with reflect.New(). This allows us to
complete all struct fields/functions even when there nil pointers.
Therefore we can drop the extra initialization which was done by some
callers.

Signed-off-by: Paul Holzinger <pholzing@redhat.com>
This commit is contained in:
Paul Holzinger 2022-04-28 14:49:03 +02:00
parent 2b8cafc067
commit f93ba587c6
No known key found for this signature in database
GPG Key ID: EB145DD938A3CAF2
5 changed files with 41 additions and 23 deletions

View File

@ -987,14 +987,12 @@ func AutocompleteFormat(o interface{}) func(cmd *cobra.Command, args []string, t
fields := strings.Split(field[len(field)-1], ".")
f := reflect.ValueOf(o)
for i := 1; i < len(fields); i++ {
if f.Kind() == reflect.Ptr {
f = f.Elem()
}
// the only supported type is struct
if f.Kind() != reflect.Struct {
val := getActualStructType(f)
if val == nil {
// no struct return nothing to complete
return nil, cobra.ShellCompDirectiveNoFileComp
}
f = *val
// last field get all names to suggest
if i == len(fields)-1 {
@ -1012,17 +1010,38 @@ func AutocompleteFormat(o interface{}) func(cmd *cobra.Command, args []string, t
}
}
// getStructFields reads all struct field names and method names and returns them.
func getStructFields(f reflect.Value, prefix string) []string {
suggestions := []string{}
// getActualStructType take the value and check if it is a struct,
// if it is pointer it will dereference it and when it is nil,
// it will create a new value from it to get the actual struct
// returns nil when type is not a struct
func getActualStructType(f reflect.Value) *reflect.Value {
// follow the pointer first
if f.Kind() == reflect.Ptr {
// if the pointer is nil we create a new value from the elements type
// this allows us to follow nil pointers and get the actual struct fields
if f.IsNil() {
f = reflect.New(f.Type().Elem())
}
f = f.Elem()
}
// we only support structs
if f.Kind() != reflect.Struct {
return nil
}
return &f
}
// getStructFields reads all struct field names and method names and returns them.
func getStructFields(f reflect.Value, prefix string) []string {
suggestions := []string{}
val := getActualStructType(f)
if val == nil {
// no struct return nothing to complete
return nil
}
f = *val
// loop over all field names
for j := 0; j < f.NumField(); j++ {
field := f.Type().Field(j)
@ -1043,7 +1062,7 @@ func getStructFields(f reflect.Value, prefix string) []string {
}
// if field is anonymous add the child fields as well
if field.Anonymous {
suggestions = append(suggestions, getStructFields(f.FieldByIndex([]int{j}), prefix)...)
suggestions = append(suggestions, getStructFields(f.Field(j), prefix)...)
}
}

View File

@ -34,10 +34,9 @@ func TestAutocompleteFormat(t *testing.T) {
Name string
Age int
Car *Car
Car2 *Car
*Anonymous
}{
Anonymous: &Anonymous{},
}
}{}
testStruct.Car = &Car{}
testStruct.Car.Extras = map[string]string{"test": "1"}
@ -80,12 +79,12 @@ func TestAutocompleteFormat(t *testing.T) {
{
"fist level struct field name",
"{{.",
[]string{"{{.Name}}", "{{.Age}}", "{{.Car.", "{{.Anonymous.", "{{.Hello}}"},
[]string{"{{.Name}}", "{{.Age}}", "{{.Car.", "{{.Car2.", "{{.Anonymous.", "{{.Hello}}"},
},
{
"fist level struct field name",
"{{ .",
[]string{"{{ .Name}}", "{{ .Age}}", "{{ .Car.", "{{ .Anonymous.", "{{ .Hello}}"},
[]string{"{{ .Name}}", "{{ .Age}}", "{{ .Car.", "{{ .Car2.", "{{ .Anonymous.", "{{ .Hello}}"},
},
{
"fist level struct field name",
@ -102,6 +101,11 @@ func TestAutocompleteFormat(t *testing.T) {
"{{ .Car.B",
[]string{"{{ .Car.Brand}}"},
},
{
"second level nil struct field name",
"{{ .Car2.",
[]string{"{{ .Car2.Brand}}", "{{ .Car2.Stats.", "{{ .Car2.Extras}}", "{{ .Car2.Color}}", "{{ .Car2.Type}}"},
},
{
"three level struct field name",
"{{ .Car.Stats.",

View File

@ -35,12 +35,7 @@ func init() {
formatFlagName := "format"
flags.StringVarP(&inspectOpts.Format, formatFlagName, "f", "json", "Format the output to a Go template or json")
_ = inspectCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(define.InspectContainerData{
State: &define.InspectContainerState{},
NetworkSettings: &define.InspectNetworkSettings{},
Config: &define.InspectContainerConfig{},
HostConfig: &define.InspectContainerHostConfig{},
}))
_ = inspectCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(define.InspectContainerData{}))
validate.AddLatestFlag(inspectCmd, &inspectOpts.Latest)
}

View File

@ -57,7 +57,7 @@ func init() {
formatFlagName := "format"
flags.StringVar(&psInput.Format, formatFlagName, "", "Pretty-print pods to JSON or using a Go template")
_ = psCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(ListPodReporter{ListPodsReport: &entities.ListPodsReport{}}))
_ = psCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(ListPodReporter{}))
flags.Bool("noheading", false, "Do not print headers")
flags.BoolVar(&psInput.Namespace, "namespace", false, "Display namespace information of the pod")

View File

@ -66,7 +66,7 @@ func infoFlags(cmd *cobra.Command) {
formatFlagName := "format"
flags.StringVarP(&inFormat, formatFlagName, "f", "", "Change the output format to JSON or a Go template")
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(define.Info{Host: &define.HostInfo{}, Store: &define.StoreInfo{}}))
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(define.Info{}))
}
func info(cmd *cobra.Command, args []string) error {