summaryrefslogtreecommitdiff
path: root/cmd/podman/common
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/podman/common')
-rw-r--r--cmd/podman/common/completion.go84
-rw-r--r--cmd/podman/common/completion_test.go142
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)
+ }
+}