diff options
Diffstat (limited to 'cmd/podman/common')
-rw-r--r-- | cmd/podman/common/completion.go | 84 | ||||
-rw-r--r-- | cmd/podman/common/completion_test.go | 142 |
2 files changed, 222 insertions, 4 deletions
diff --git a/cmd/podman/common/completion.go b/cmd/podman/common/completion.go index 6086df297..4aca79770 100644 --- a/cmd/podman/common/completion.go +++ b/cmd/podman/common/completion.go @@ -4,6 +4,7 @@ import ( "bufio" "fmt" "os" + "reflect" "strings" "github.com/containers/common/pkg/config" @@ -891,10 +892,85 @@ func AutocompleteNetworkFlag(cmd *cobra.Command, args []string, toComplete strin return append(networks, suggestions...), dir } -// AutocompleteJSONFormat - Autocomplete format flag option. -// -> "json" -func AutocompleteJSONFormat(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return []string{"json"}, cobra.ShellCompDirectiveNoFileComp +// 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. +// 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) { + // this function provides shell completion for go templates + return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + // autocomplete json when nothing or json is typed + if strings.HasPrefix("json", toComplete) { + return []string{"json"}, cobra.ShellCompDirectiveNoFileComp + } + // no input struct we cannot provide completion return nothing + if o == nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + // toComplete could look like this: {{ .Config }} {{ .Field.F + // 1. split the template variable delimiter + vars := strings.Split(toComplete, "{{") + if len(vars) == 1 { + // no variables return no completion + return nil, cobra.ShellCompDirectiveNoFileComp + } + // clean the spaces from the last var + field := strings.Split(vars[len(vars)-1], " ") + // split this into it struct field names + 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 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + // last field get all names to suggest + if i == len(fields)-1 { + suggestions := []string{} + for j := 0; j < f.NumField(); j++ { + fname := f.Type().Field(j).Name + suffix := "}}" + kind := f.Type().Field(j).Type.Kind() + if kind == reflect.Ptr { + // make sure to read the actual type when it is a pointer + kind = f.Type().Field(j).Type.Elem().Kind() + } + // when we have a nested struct do not append braces instead append a dot + if kind == reflect.Struct { + suffix = "." + } + if strings.HasPrefix(fname, fields[i]) { + // add field name with closing braces + suggestions = append(suggestions, fname+suffix) + } + } + + for j := 0; j < f.NumMethod(); j++ { + fname := f.Type().Method(j).Name + if strings.HasPrefix(fname, fields[i]) { + // add method name with closing braces + suggestions = append(suggestions, fname+"}}") + } + } + + // add the current toComplete value in front so that the shell can complete this correctly + toCompArr := strings.Split(toComplete, ".") + toCompArr[len(toCompArr)-1] = "" + toComplete = strings.Join(toCompArr, ".") + return prefixSlice(toComplete, suggestions), cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp + } + // set the next struct field + f = f.FieldByName(fields[i]) + } + return nil, cobra.ShellCompDirectiveNoFileComp + } } // AutocompleteEventFilter - Autocomplete event filter flag options. diff --git a/cmd/podman/common/completion_test.go b/cmd/podman/common/completion_test.go new file mode 100644 index 000000000..5bd627b85 --- /dev/null +++ b/cmd/podman/common/completion_test.go @@ -0,0 +1,142 @@ +package common_test + +import ( + "testing" + + "github.com/containers/podman/v3/cmd/podman/common" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" +) + +type Car struct { + Brand string + Stats struct { + HP *int + Displacement int + } + Extras map[string]string +} + +func (c Car) Type() string { + return "" +} + +func (c Car) Color() string { + return "" +} + +func TestAutocompleteFormat(t *testing.T) { + testStruct := struct { + Name string + Age int + Car *Car + }{} + + testStruct.Car = &Car{} + testStruct.Car.Extras = map[string]string{"test": "1"} + + tests := []struct { + name string + toComplete string + expected []string + }{ + { + "empty completion", + "", + []string{"json"}, + }, + { + "json completion", + "json", + []string{"json"}, + }, + { + "invalid completion", + "blahblah", + nil, + }, + { + "invalid completion", + "{{", + nil, + }, + { + "invalid completion", + "{{ ", + nil, + }, + { + "invalid completion", + "{{ ..", + nil, + }, + { + "fist level struct field name", + "{{.", + []string{"{{.Name}}", "{{.Age}}", "{{.Car."}, + }, + { + "fist level struct field name", + "{{ .", + []string{"{{ .Name}}", "{{ .Age}}", "{{ .Car."}, + }, + { + "fist level struct field name", + "{{ .N", + []string{"{{ .Name}}"}, + }, + { + "second level struct field name", + "{{ .Car.", + []string{"{{ .Car.Brand}}", "{{ .Car.Stats.", "{{ .Car.Extras}}", "{{ .Car.Color}}", "{{ .Car.Type}}"}, + }, + { + "second level struct field name", + "{{ .Car.B", + []string{"{{ .Car.Brand}}"}, + }, + { + "three level struct field name", + "{{ .Car.Stats.", + []string{"{{ .Car.Stats.HP}}", "{{ .Car.Stats.Displacement}}"}, + }, + { + "three level struct field name", + "{{ .Car.Stats.D", + []string{"{{ .Car.Stats.Displacement}}"}, + }, + { + "second level struct field name", + "{{ .Car.B", + []string{"{{ .Car.Brand}}"}, + }, + { + "invalid field name", + "{{ .Ca.B", + nil, + }, + { + "map key names don't work", + "{{ .Car.Extras.", + nil, + }, + { + "two variables struct field name", + "{{ .Car.Brand }} {{ .Car.", + []string{"{{ .Car.Brand }} {{ .Car.Brand}}", "{{ .Car.Brand }} {{ .Car.Stats.", "{{ .Car.Brand }} {{ .Car.Extras}}", + "{{ .Car.Brand }} {{ .Car.Color}}", "{{ .Car.Brand }} {{ .Car.Type}}"}, + }, + { + "only dot without variable", + ".", + nil, + }, + } + + for _, test := range tests { + 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) + } +} |