Skip to content

Commit

Permalink
shell completion --format: support maps and functions
Browse files Browse the repository at this point in the history
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 containers#14223

Signed-off-by: Paul Holzinger <[email protected]>
  • Loading branch information
Luap99 authored and cdoern committed May 27, 2022
1 parent b2cfc46 commit f8157de
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 31 deletions.
76 changes: 52 additions & 24 deletions cmd/podman/common/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -978,7 +978,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) {
Expand Down Expand Up @@ -1007,6 +1007,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 {
Expand All @@ -1018,39 +1024,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.
Expand All @@ -1060,12 +1083,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
Expand All @@ -1084,7 +1106,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
Expand Down Expand Up @@ -1117,10 +1139,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
Expand Down
40 changes: 33 additions & 7 deletions cmd/podman/common/completion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -63,7 +74,6 @@ func TestAutocompleteFormat(t *testing.T) {
}{}

testStruct.Car = &Car{}
testStruct.Car.Extras = map[string]string{"test": "1"}

tests := []struct {
name string
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -155,21 +165,37 @@ func TestAutocompleteFormat(t *testing.T) {
"{{ .Car.Extras.",
[]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)
Expand Down

0 comments on commit f8157de

Please sign in to comment.