Skip to content

Commit

Permalink
Add go template shell completion for --format
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
Paul Holzinger committed Apr 21, 2021
1 parent 979f047 commit d81021e
Show file tree
Hide file tree
Showing 27 changed files with 258 additions and 28 deletions.
84 changes: 80 additions & 4 deletions cmd/podman/common/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bufio"
"fmt"
"os"
"reflect"
"strings"

"github.com/containers/common/pkg/config"
Expand Down Expand Up @@ -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.
Expand Down
142 changes: 142 additions & 0 deletions cmd/podman/common/completion_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
2 changes: 1 addition & 1 deletion cmd/podman/containers/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func init() {

formatFlagName := "format"
flags.StringVar(&diffOpts.Format, formatFlagName, "", "Change the output format")
_ = diffCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = diffCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(nil))

validate.AddLatestFlag(diffCmd, &diffOpts.Latest)
}
Expand Down
8 changes: 7 additions & 1 deletion cmd/podman/containers/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"github.com/containers/podman/v3/cmd/podman/inspect"
"github.com/containers/podman/v3/cmd/podman/registry"
"github.com/containers/podman/v3/cmd/podman/validate"
"github.com/containers/podman/v3/libpod/define"
"github.com/containers/podman/v3/pkg/domain/entities"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -35,7 +36,12 @@ func init() {

formatFlagName := "format"
flags.StringVarP(&inspectOpts.Format, formatFlagName, "f", "json", "Format the output to a Go template or json")
_ = inspectCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = inspectCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(define.InspectContainerData{
State: &define.InspectContainerState{},
NetworkSettings: &define.InspectNetworkSettings{},
Config: &define.InspectContainerConfig{},
HostConfig: &define.InspectContainerHostConfig{},
}))

validate.AddLatestFlag(inspectCmd, &inspectOpts.Latest)
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/podman/containers/mount.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func mountFlags(cmd *cobra.Command) {

formatFlagName := "format"
flags.StringVar(&mountOpts.Format, formatFlagName, "", "Print the mounted containers in specified format (json)")
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(nil))

flags.BoolVar(&mountOpts.NoTruncate, "notruncate", false, "Do not truncate output")
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/podman/containers/ps.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func listFlagSet(cmd *cobra.Command) {

formatFlagName := "format"
flags.StringVar(&listOpts.Format, formatFlagName, "", "Pretty-print containers to JSON or using a Go template")
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(entities.ListContainer{}))

lastFlagName := "last"
flags.IntVarP(&listOpts.Last, lastFlagName, "n", -1, "Print the n last created containers (all states)")
Expand Down
2 changes: 1 addition & 1 deletion cmd/podman/containers/stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func statFlags(cmd *cobra.Command) {

formatFlagName := "format"
flags.StringVar(&statsOptions.Format, formatFlagName, "", "Pretty-print container statistics to JSON or using a Go template")
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(define.ContainerStats{}))

flags.BoolVar(&statsOptions.NoReset, "no-reset", false, "Disable resetting the screen between intervals")
flags.BoolVar(&statsOptions.NoStream, "no-stream", false, "Disable streaming stats and only pull the first result, default setting is false")
Expand Down
2 changes: 1 addition & 1 deletion cmd/podman/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func init() {

formatFlagName := "format"
flags.StringVar(&diffOpts.Format, formatFlagName, "", "Change the output format")
_ = diffCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = diffCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(nil))

validate.AddLatestFlag(diffCmd, &diffOpts.Latest)
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/podman/generate/systemd.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func init() {

formatFlagName := "format"
flags.StringVar(&format, formatFlagName, "", "Print the created units in specified format (json)")
_ = systemdCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = systemdCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(nil))

flags.SetNormalizeFunc(utils.AliasFlags)
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/podman/images/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func diffFlags(flags *pflag.FlagSet) {

formatFlagName := "format"
flags.StringVar(&diffOpts.Format, formatFlagName, "", "Change the output format")
_ = diffCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = diffCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(nil))
}

func diff(cmd *cobra.Command, args []string) error {
Expand Down
2 changes: 1 addition & 1 deletion cmd/podman/images/history.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func historyFlags(cmd *cobra.Command) {

formatFlagName := "format"
flags.StringVar(&opts.format, formatFlagName, "", "Change the output to JSON or a Go template")
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(entities.ImageHistoryLayer{}))

flags.BoolVarP(&opts.human, "human", "H", true, "Display sizes and dates in human readable format")
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate the output")
Expand Down
3 changes: 2 additions & 1 deletion cmd/podman/images/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"github.com/containers/podman/v3/cmd/podman/inspect"
"github.com/containers/podman/v3/cmd/podman/registry"
"github.com/containers/podman/v3/pkg/domain/entities"
inspectTypes "github.com/containers/podman/v3/pkg/inspect"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -34,7 +35,7 @@ func init() {

formatFlagName := "format"
flags.StringVarP(&inspectOpts.Format, formatFlagName, "f", "json", "Format the output to a Go template or json")
_ = inspectCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = inspectCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(inspectTypes.ImageData{}))
}

func inspectExec(cmd *cobra.Command, args []string) error {
Expand Down
2 changes: 1 addition & 1 deletion cmd/podman/images/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func imageListFlagSet(cmd *cobra.Command) {

formatFlagName := "format"
flags.StringVar(&listFlag.format, formatFlagName, "", "Change the output format to JSON or a Go template")
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(entities.ImageSummary{}))

flags.BoolVar(&listFlag.digests, "digests", false, "Show digests")
flags.BoolVarP(&listFlag.noHeading, "noheading", "n", false, "Do not print column headings")
Expand Down
2 changes: 1 addition & 1 deletion cmd/podman/images/mount.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func mountFlags(cmd *cobra.Command) {

formatFlagName := "format"
flags.StringVar(&mountOpts.Format, formatFlagName, "", "Print the mounted images in specified format (json)")
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(nil))
}

func init() {
Expand Down
2 changes: 1 addition & 1 deletion cmd/podman/networks/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func init() {

formatFlagName := "format"
flags.StringVarP(&inspectOpts.Format, formatFlagName, "f", "", "Pretty-print network to JSON or using a Go template")
_ = networkinspectCommand.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = networkinspectCommand.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(nil))
}

func networkInspect(_ *cobra.Command, args []string) error {
Expand Down
2 changes: 1 addition & 1 deletion cmd/podman/networks/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ var (
func networkListFlags(flags *pflag.FlagSet) {
formatFlagName := "format"
flags.StringVar(&networkListOptions.Format, formatFlagName, "", "Pretty-print networks to JSON or using a Go template")
_ = networklistCommand.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat)
_ = networklistCommand.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(ListPrintReports{}))

flags.BoolVarP(&networkListOptions.Quiet, "quiet", "q", false, "display only names")
flags.BoolVar(&noTrunc, "no-trunc", false, "Do not truncate the network ID")
Expand Down
Loading

0 comments on commit d81021e

Please sign in to comment.