Skip to content

Commit

Permalink
Merge pull request containers#10099 from Luap99/format-completion
Browse files Browse the repository at this point in the history
Add go template shell completion for --format
  • Loading branch information
openshift-merge-robot authored Apr 22, 2021
2 parents 21c7784 + d81021e commit 8465626
Show file tree
Hide file tree
Showing 26 changed files with 257 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 8465626

Please sign in to comment.