diff options
author | Paul Holzinger <pholzing@redhat.com> | 2022-05-16 20:38:45 +0200 |
---|---|---|
committer | Paul Holzinger <pholzing@redhat.com> | 2022-05-18 18:28:22 +0200 |
commit | 3d8a1f91731b7935ae5239023d588a028dcd51e8 (patch) | |
tree | 3e6b1a8fac6fa63d292df2980ea44470b8205d37 /cmd/podman/common | |
parent | ecd6edb19186b43c064a09b0a824732ff5f5242e (diff) | |
download | podman-3d8a1f91731b7935ae5239023d588a028dcd51e8.tar.gz podman-3d8a1f91731b7935ae5239023d588a028dcd51e8.tar.bz2 podman-3d8a1f91731b7935ae5239023d588a028dcd51e8.zip |
shell completion --format: support maps and functions
Currently we only support structs in a template string like this:
`{{.var1.test.` -> this meams that test must be a struct field on var1.
Now with this var1 and test could also be either a map or function which
returns a struct.
A actual example:
`podman container inspect --format {{.NetworkSettings.Networks.netname.`
Now we can complete the struct fileds after netname. Note that this
cannot complete map keys since they are empty by default, so it is
impossible to get them in the completion logic.
Also this fixes a panic with embeeded nil structs
Fixes #14223
Signed-off-by: Paul Holzinger <pholzing@redhat.com>
Diffstat (limited to 'cmd/podman/common')
-rw-r--r-- | cmd/podman/common/completion.go | 76 | ||||
-rw-r--r-- | cmd/podman/common/completion_test.go | 40 |
2 files changed, 85 insertions, 31 deletions
diff --git a/cmd/podman/common/completion.go b/cmd/podman/common/completion.go index 4fec549c1..32dd896c0 100644 --- a/cmd/podman/common/completion.go +++ b/cmd/podman/common/completion.go @@ -975,7 +975,7 @@ func convertFormatSuggestions(suggestions []formatSuggestion) []string { // AutocompleteFormat - Autocomplete json or a given struct to use for a go template. // The input can be nil, In this case only json will be autocompleted. -// This function will only work for structs other types are not supported. +// This function will only work for pointer to structs other types are not supported. // When "{{." is typed the field and method names of the given struct will be completed. // This also works recursive for nested structs. func AutocompleteFormat(o interface{}) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { @@ -1004,6 +1004,12 @@ func AutocompleteFormat(o interface{}) func(cmd *cobra.Command, args []string, t // split this into it struct field names fields := strings.Split(field[len(field)-1], ".") f := reflect.ValueOf(o) + if f.Kind() != reflect.Ptr { + // We panic here to make sure that all callers pass the value by reference. + // If someone passes a by value then all podman commands will panic since + // this function is run at init time. + panic("AutocompleteFormat: passed value must be a pointer to a struct") + } for i := 1; i < len(fields); i++ { // last field get all names to suggest if i == len(fields)-1 { @@ -1015,39 +1021,56 @@ func AutocompleteFormat(o interface{}) func(cmd *cobra.Command, args []string, t return prefixSlice(toComplete, convertFormatSuggestions(suggestions)), cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp } - val := getActualStructType(f) - if val == nil { - // no struct return nothing to complete + // first follow pointer and create element when it is nil + f = actualReflectValue(f) + switch f.Kind() { + case reflect.Struct: + for j := 0; j < f.NumField(); j++ { + field := f.Type().Field(j) + // ok this is a bit weird but when we have an embedded nil struct + // calling FieldByName on a name which is present on this struct will panic + // Therefore we have to init them (non nil ptr), https://github.com/containers/podman/issues/14223 + if field.Anonymous && f.Field(j).Type().Kind() == reflect.Ptr { + f.Field(j).Set(reflect.New(f.Field(j).Type().Elem())) + } + } + // set the next struct field + f = f.FieldByName(fields[i]) + case reflect.Map: + rtype := f.Type().Elem() + if rtype.Kind() == reflect.Ptr { + rtype = rtype.Elem() + } + f = reflect.New(rtype) + case reflect.Func: + if f.Type().NumOut() != 1 { + // unsupported type return nothing + return nil, cobra.ShellCompDirectiveNoFileComp + } + f = reflect.New(f.Type().Out(0)) + default: + // unsupported type return nothing return nil, cobra.ShellCompDirectiveNoFileComp } - f = *val - - // set the next struct field - f = f.FieldByName(fields[i]) } return nil, cobra.ShellCompDirectiveNoFileComp } } -// getActualStructType take the value and check if it is a struct, +// actualReflectValue takes the value, // 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 { +// it will create a new value from it +func actualReflectValue(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 + // this allows us to follow nil pointers and get the actual type 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 + return f } // getStructFields reads all struct field names and method names and returns them. @@ -1057,12 +1080,11 @@ func getStructFields(f reflect.Value, prefix string) []formatSuggestion { suggestions = append(suggestions, getMethodNames(f, prefix)...) } - val := getActualStructType(f) - if val == nil { - // no struct return nothing to complete + f = actualReflectValue(f) + // we only support structs + if f.Kind() != reflect.Struct { return suggestions } - f = *val var anonymous []formatSuggestion // loop over all field names @@ -1081,7 +1103,7 @@ func getStructFields(f reflect.Value, prefix string) []formatSuggestion { kind = field.Type.Elem().Kind() } // when we have a nested struct do not append braces instead append a dot - if kind == reflect.Struct { + if kind == reflect.Struct || kind == reflect.Map { suffix = "." } // if field is anonymous add the child fields as well @@ -1114,10 +1136,16 @@ func getMethodNames(f reflect.Value, prefix string) []formatSuggestion { if method.Func.Type().NumOut() != 1 { continue } + // when we have a nested struct do not append braces instead append a dot + kind := method.Func.Type().Out(0).Kind() + suffix := "}}" + if kind == reflect.Struct || kind == reflect.Map { + suffix = "." + } fname := method.Name if strings.HasPrefix(fname, prefix) { // add method name with closing braces - suggestions = append(suggestions, formatSuggestion{fieldname: fname, suffix: "}}"}) + suggestions = append(suggestions, formatSuggestion{fieldname: fname, suffix: suffix}) } } return suggestions diff --git a/cmd/podman/common/completion_test.go b/cmd/podman/common/completion_test.go index 34ec9b6a5..ae23b02e2 100644 --- a/cmd/podman/common/completion_test.go +++ b/cmd/podman/common/completion_test.go @@ -14,7 +14,14 @@ type Car struct { HP *int Displacement int } - Extras map[string]string + Extras map[string]Extra + // also ensure it will work with pointers + Extras2 map[string]*Extra +} + +type Extra struct { + Name1 string + Name2 string } type Anonymous struct { @@ -52,6 +59,10 @@ func (c Car) TwoOut() (string, string) { return "", "" } +func (c Car) Struct() Car { + return Car{} +} + func TestAutocompleteFormat(t *testing.T) { testStruct := struct { Name string @@ -63,7 +74,6 @@ func TestAutocompleteFormat(t *testing.T) { }{} testStruct.Car = &Car{} - testStruct.Car.Extras = map[string]string{"test": "1"} tests := []struct { name string @@ -118,7 +128,7 @@ func TestAutocompleteFormat(t *testing.T) { { "second level struct field name", "{{ .Car.", - []string{"{{ .Car.Color}}", "{{ .Car.Type}}", "{{ .Car.Brand}}", "{{ .Car.Stats.", "{{ .Car.Extras}}"}, + []string{"{{ .Car.Color}}", "{{ .Car.Struct.", "{{ .Car.Type}}", "{{ .Car.Brand}}", "{{ .Car.Stats.", "{{ .Car.Extras.", "{{ .Car.Extras2."}, }, { "second level struct field name", @@ -128,7 +138,7 @@ func TestAutocompleteFormat(t *testing.T) { { "second level nil struct field name", "{{ .Car2.", - []string{"{{ .Car2.Color}}", "{{ .Car2.Type}}", "{{ .Car2.Brand}}", "{{ .Car2.Stats.", "{{ .Car2.Extras}}"}, + []string{"{{ .Car2.Color}}", "{{ .Car2.Struct.", "{{ .Car2.Type}}", "{{ .Car2.Brand}}", "{{ .Car2.Stats.", "{{ .Car2.Extras.", "{{ .Car2.Extras2."}, }, { "three level struct field name", @@ -156,20 +166,36 @@ func TestAutocompleteFormat(t *testing.T) { []string{}, }, { + "map values work", + "{{ .Car.Extras.somekey.", + []string{"{{ .Car.Extras.somekey.Name1}}", "{{ .Car.Extras.somekey.Name2}}"}, + }, + { + "map values work with ptr", + "{{ .Car.Extras2.somekey.", + []string{"{{ .Car.Extras2.somekey.Name1}}", "{{ .Car.Extras2.somekey.Name2}}"}, + }, + { "two variables struct field name", "{{ .Car.Brand }} {{ .Car.", - []string{"{{ .Car.Brand }} {{ .Car.Color}}", "{{ .Car.Brand }} {{ .Car.Type}}", "{{ .Car.Brand }} {{ .Car.Brand}}", - "{{ .Car.Brand }} {{ .Car.Stats.", "{{ .Car.Brand }} {{ .Car.Extras}}"}, + []string{"{{ .Car.Brand }} {{ .Car.Color}}", "{{ .Car.Brand }} {{ .Car.Struct.", "{{ .Car.Brand }} {{ .Car.Type}}", + "{{ .Car.Brand }} {{ .Car.Brand}}", "{{ .Car.Brand }} {{ .Car.Stats.", "{{ .Car.Brand }} {{ .Car.Extras.", + "{{ .Car.Brand }} {{ .Car.Extras2."}, }, { "only dot without variable", ".", nil, }, + { + "access embedded nil struct field", + "{{.Hello.", + []string{}, + }, } for _, test := range tests { - completion, directive := common.AutocompleteFormat(testStruct)(nil, nil, test.toComplete) + completion, directive := common.AutocompleteFormat(&testStruct)(nil, nil, test.toComplete) // directive should always be greater than ShellCompDirectiveNoFileComp assert.GreaterOrEqual(t, directive, cobra.ShellCompDirectiveNoFileComp, "unexpected ShellCompDirective") assert.Equal(t, test.expected, completion, test.name) |