From d81021ed265e3cdfe32cdd0082b139f796ff5bfa Mon Sep 17 00:00:00 2001 From: Paul Holzinger Date: Wed, 21 Apr 2021 11:32:03 +0200 Subject: Add go template shell completion for --format The --format flags accepts go template strings. I use this often but I consistently forget the field names. This commit adds a way to provide shell completion for the --format flag. It works by automatically receiving the field names with the reflect package from the given struct. This requires almost no maintenance since this ensures that we always use the correct field names. This also works for nested structs. ``` $ podman ps --format "{{.P" {{.Pid}} {{.PIDNS}} {{.Pod}} {{.PodName}} {{.Ports}} ``` NOTE: This only works when you use quotes otherwise the shell does not provide completions. Also this does not work for fish at the moment. Signed-off-by: Paul Holzinger --- cmd/podman/common/completion.go | 84 ++++++++++++++++++++- cmd/podman/common/completion_test.go | 142 +++++++++++++++++++++++++++++++++++ 2 files changed, 222 insertions(+), 4 deletions(-) create mode 100644 cmd/podman/common/completion_test.go (limited to 'cmd/podman/common') 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) + } +} -- cgit v1.2.3-54-g00ecf