zsh-completions: revised flags completion rendering + new features:
- If the flags are not bool the completion expects argument. - You don't have to specify file extensions for file completion to work. - Allow multiple occurrences of flag if type is stringArray. Need to verify that these assumption are correct :)
This commit is contained in:
		
				
					committed by
					
						
						Steve Francia
					
				
			
			
				
	
			
			
			
						parent
						
							e8018e8612
						
					
				
				
					commit
					ec4b8c974c
				
			@ -4,6 +4,7 @@ import (
 | 
				
			|||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"io"
 | 
						"io"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
	"text/template"
 | 
						"text/template"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/spf13/pflag"
 | 
						"github.com/spf13/pflag"
 | 
				
			||||||
@ -13,24 +14,9 @@ var (
 | 
				
			|||||||
	funcMap = template.FuncMap{
 | 
						funcMap = template.FuncMap{
 | 
				
			||||||
		"genZshFuncName":              generateZshCompletionFuncName,
 | 
							"genZshFuncName":              generateZshCompletionFuncName,
 | 
				
			||||||
		"extractFlags":                extractFlags,
 | 
							"extractFlags":                extractFlags,
 | 
				
			||||||
		"simpleFlag":     simpleFlag,
 | 
							"genFlagEntryForZshArguments": genFlagEntryForZshArguments,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	zshCompletionText = `
 | 
						zshCompletionText = `
 | 
				
			||||||
{{/* for pflag.Flag (specifically annotations) */}}
 | 
					 | 
				
			||||||
{{define "flagAnnotations" -}}
 | 
					 | 
				
			||||||
{{with index .Annotations "cobra_annotation_bash_completion_filename_extensions"}}:filename:_files{{end}}
 | 
					 | 
				
			||||||
{{- end}}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
{{/* for pflag.Flag with short and long options */}}
 | 
					 | 
				
			||||||
{{define "complexFlag" -}}
 | 
					 | 
				
			||||||
"(-{{.Shorthand}} --{{.Name}})"{-{{.Shorthand}},--{{.Name}}}"[{{.Usage}}]{{template "flagAnnotations" .}}"
 | 
					 | 
				
			||||||
{{- end}}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
{{/* for pflag.Flag with either short or long options */}}
 | 
					 | 
				
			||||||
{{define "simpleFlag" -}}
 | 
					 | 
				
			||||||
"{{with .Name}}--{{.}}{{else}}-{{.Shorthand}}{{end}}[{{.Usage}}]{{template "flagAnnotations" .}}"
 | 
					 | 
				
			||||||
{{- end}}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
{{/* should accept Command (that contains subcommands) as parameter */}}
 | 
					{{/* should accept Command (that contains subcommands) as parameter */}}
 | 
				
			||||||
{{define "argumentsC" -}}
 | 
					{{define "argumentsC" -}}
 | 
				
			||||||
{{ $cmdPath := genZshFuncName .}}
 | 
					{{ $cmdPath := genZshFuncName .}}
 | 
				
			||||||
@ -38,7 +24,7 @@ function {{$cmdPath}} {
 | 
				
			|||||||
  local -a commands
 | 
					  local -a commands
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  _arguments -C \{{- range extractFlags .}}
 | 
					  _arguments -C \{{- range extractFlags .}}
 | 
				
			||||||
    {{if simpleFlag .}}{{template "simpleFlag" .}}{{else}}{{template "complexFlag" .}}{{end}} \{{- end}}
 | 
					    {{genFlagEntryForZshArguments .}} \{{- end}}
 | 
				
			||||||
    "1: :->cmnds" \
 | 
					    "1: :->cmnds" \
 | 
				
			||||||
    "*::arg:->args"
 | 
					    "*::arg:->args"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -66,7 +52,7 @@ function {{$cmdPath}} {
 | 
				
			|||||||
{{define "arguments" -}}
 | 
					{{define "arguments" -}}
 | 
				
			||||||
function {{genZshFuncName .}} {
 | 
					function {{genZshFuncName .}} {
 | 
				
			||||||
{{"  _arguments"}}{{range extractFlags .}} \
 | 
					{{"  _arguments"}}{{range extractFlags .}} \
 | 
				
			||||||
    {{if simpleFlag .}}{{template "simpleFlag" .}}{{else}}{{template "complexFlag" .}}{{end -}}
 | 
					    {{genFlagEntryForZshArguments . -}}
 | 
				
			||||||
{{end}}
 | 
					{{end}}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
{{end}}
 | 
					{{end}}
 | 
				
			||||||
@ -132,3 +118,56 @@ func extractFlags(c *Command) []*pflag.Flag {
 | 
				
			|||||||
func simpleFlag(p *pflag.Flag) bool {
 | 
					func simpleFlag(p *pflag.Flag) bool {
 | 
				
			||||||
	return p.Name == "" || p.Shorthand == ""
 | 
						return p.Name == "" || p.Shorthand == ""
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// genFlagEntryForZshArguments returns an entry that matches _arguments
 | 
				
			||||||
 | 
					// zsh-completion parameters. It's too complicated to generate in a template.
 | 
				
			||||||
 | 
					func genFlagEntryForZshArguments(f *pflag.Flag) string {
 | 
				
			||||||
 | 
						if f.Name == "" || f.Shorthand == "" {
 | 
				
			||||||
 | 
							return genFlagEntryForSingleOptionFlag(f)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return genFlagEntryForMultiOptionFlag(f)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func genFlagEntryForSingleOptionFlag(f *pflag.Flag) string {
 | 
				
			||||||
 | 
						var option, multiMark, extras string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if f.Value.Type() == "stringArray" {
 | 
				
			||||||
 | 
							multiMark = "*"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						option = "--" + f.Name
 | 
				
			||||||
 | 
						if option == "--" {
 | 
				
			||||||
 | 
							option = "-" + f.Shorthand
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						extras = genZshFlagEntryExtras(f)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return fmt.Sprintf(`"%s%s[%s]%s"`, multiMark, option, f.Usage, extras)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func genFlagEntryForMultiOptionFlag(f *pflag.Flag) string {
 | 
				
			||||||
 | 
						var options, parenMultiMark, curlyMultiMark, extras string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if f.Value.Type() == "stringArray" {
 | 
				
			||||||
 | 
							parenMultiMark = "*"
 | 
				
			||||||
 | 
							curlyMultiMark = "\\*"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						options = fmt.Sprintf(`"(%s-%s %s--%s)"{%s-%s,%s--%s}`,
 | 
				
			||||||
 | 
							parenMultiMark, f.Shorthand, parenMultiMark, f.Name, curlyMultiMark, f.Shorthand, curlyMultiMark, f.Name)
 | 
				
			||||||
 | 
						extras = genZshFlagEntryExtras(f)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return fmt.Sprintf(`%s"[%s]%s"`, options, f.Usage, extras)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func genZshFlagEntryExtras(f *pflag.Flag) string {
 | 
				
			||||||
 | 
						var extras string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_, pathSpecified := f.Annotations[BashCompFilenameExt]
 | 
				
			||||||
 | 
						if pathSpecified {
 | 
				
			||||||
 | 
							extras = ":filename:_files"
 | 
				
			||||||
 | 
						} else if !strings.HasPrefix(f.Value.Type(), "bool") {
 | 
				
			||||||
 | 
							extras = ":" // allow option variable without assisting
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return extras
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -48,7 +48,7 @@ func TestGenZshCompletion(t *testing.T) {
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			name: "command with subcommands",
 | 
								name: "command with subcommands and flags with values",
 | 
				
			||||||
			root: func() *Command {
 | 
								root: func() *Command {
 | 
				
			||||||
				r := &Command{
 | 
									r := &Command{
 | 
				
			||||||
					Use:  "rootcmd",
 | 
										Use:  "rootcmd",
 | 
				
			||||||
@ -75,7 +75,7 @@ func TestGenZshCompletion(t *testing.T) {
 | 
				
			|||||||
				`_arguments -C \\\n.*"--debug\[description]"`,
 | 
									`_arguments -C \\\n.*"--debug\[description]"`,
 | 
				
			||||||
				`function _rootcmd_subcmd1 {`,
 | 
									`function _rootcmd_subcmd1 {`,
 | 
				
			||||||
				`function _rootcmd_subcmd1 {`,
 | 
									`function _rootcmd_subcmd1 {`,
 | 
				
			||||||
				`_arguments \\\n.*"\(-o --option\)"{-o,--option}"\[option description]" \\\n`,
 | 
									`_arguments \\\n.*"\(-o --option\)"{-o,--option}"\[option description]:" \\\n`,
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
@ -88,20 +88,35 @@ func TestGenZshCompletion(t *testing.T) {
 | 
				
			|||||||
					Run:   emptyRun,
 | 
										Run:   emptyRun,
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				r.Flags().StringVarP(&file, "config", "c", file, "config file")
 | 
									r.Flags().StringVarP(&file, "config", "c", file, "config file")
 | 
				
			||||||
				r.MarkFlagFilename("config", "ext")
 | 
									r.MarkFlagFilename("config")
 | 
				
			||||||
				return r
 | 
									return r
 | 
				
			||||||
			}(),
 | 
								}(),
 | 
				
			||||||
			expectedExpressions: []string{
 | 
								expectedExpressions: []string{
 | 
				
			||||||
				`\n +"\(-c --config\)"{-c,--config}"\[config file]:filename:_files"`,
 | 
									`\n +"\(-c --config\)"{-c,--config}"\[config file]:filename:_files"`,
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "repeated variables both with and without value",
 | 
				
			||||||
 | 
								root: func() *Command {
 | 
				
			||||||
 | 
									r := genTestCommand("mycmd", true)
 | 
				
			||||||
 | 
									_ = r.Flags().StringArrayP("debug", "d", []string{}, "debug usage")
 | 
				
			||||||
 | 
									_ = r.Flags().StringArray("option", []string{}, "options")
 | 
				
			||||||
 | 
									return r
 | 
				
			||||||
 | 
								}(),
 | 
				
			||||||
 | 
								expectedExpressions: []string{
 | 
				
			||||||
 | 
									`"\*--option\[options]`,
 | 
				
			||||||
 | 
									`"\(\*-d \*--debug\)"{\\\*-d,\\\*--debug}`,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for _, tc := range tcs {
 | 
						for _, tc := range tcs {
 | 
				
			||||||
		t.Run(tc.name, func(t *testing.T) {
 | 
							t.Run(tc.name, func(t *testing.T) {
 | 
				
			||||||
			tc.root.Execute()
 | 
								tc.root.Execute()
 | 
				
			||||||
			buf := new(bytes.Buffer)
 | 
								buf := new(bytes.Buffer)
 | 
				
			||||||
			tc.root.GenZshCompletion(buf)
 | 
								if err := tc.root.GenZshCompletion(buf); err != nil {
 | 
				
			||||||
 | 
									t.Error(err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
			output := buf.Bytes()
 | 
								output := buf.Bytes()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			for _, expr := range tc.expectedExpressions {
 | 
								for _, expr := range tc.expectedExpressions {
 | 
				
			||||||
@ -173,7 +188,9 @@ func TestGenZshCompletionHidden(t *testing.T) {
 | 
				
			|||||||
		t.Run(tc.name, func(t *testing.T) {
 | 
							t.Run(tc.name, func(t *testing.T) {
 | 
				
			||||||
			tc.root.Execute()
 | 
								tc.root.Execute()
 | 
				
			||||||
			buf := new(bytes.Buffer)
 | 
								buf := new(bytes.Buffer)
 | 
				
			||||||
			tc.root.GenZshCompletion(buf)
 | 
								if err := tc.root.GenZshCompletion(buf); err != nil {
 | 
				
			||||||
 | 
									t.Error(err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
			output := buf.String()
 | 
								output := buf.String()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			for _, expr := range tc.expectedExpressions {
 | 
								for _, expr := range tc.expectedExpressions {
 | 
				
			||||||
@ -230,6 +247,7 @@ func constructLargeCommandHeirarchy() *Command {
 | 
				
			|||||||
	var config, st1, st2 string
 | 
						var config, st1, st2 string
 | 
				
			||||||
	var long, debug bool
 | 
						var long, debug bool
 | 
				
			||||||
	var in1, in2 int
 | 
						var in1, in2 int
 | 
				
			||||||
 | 
						var verbose []bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	r := genTestCommand("mycmd", false)
 | 
						r := genTestCommand("mycmd", false)
 | 
				
			||||||
	r.PersistentFlags().StringVarP(&config, "config", "c", config, "config usage")
 | 
						r.PersistentFlags().StringVarP(&config, "config", "c", config, "config usage")
 | 
				
			||||||
@ -238,6 +256,8 @@ func constructLargeCommandHeirarchy() *Command {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	s1 := genTestCommand("sub1", true)
 | 
						s1 := genTestCommand("sub1", true)
 | 
				
			||||||
	s1.Flags().BoolVar(&long, "long", long, "long descriptin")
 | 
						s1.Flags().BoolVar(&long, "long", long, "long descriptin")
 | 
				
			||||||
 | 
						s1.Flags().BoolSliceVar(&verbose, "verbose", verbose, "verbose description")
 | 
				
			||||||
 | 
						s1.Flags().StringArray("option", []string{}, "various options")
 | 
				
			||||||
	s2 := genTestCommand("sub2", true)
 | 
						s2 := genTestCommand("sub2", true)
 | 
				
			||||||
	s2.PersistentFlags().BoolVar(&debug, "debug", debug, "debug description")
 | 
						s2.PersistentFlags().BoolVar(&debug, "debug", debug, "debug description")
 | 
				
			||||||
	s3 := genTestCommand("sub3", true)
 | 
						s3 := genTestCommand("sub3", true)
 | 
				
			||||||
@ -249,6 +269,7 @@ func constructLargeCommandHeirarchy() *Command {
 | 
				
			|||||||
	s1_3 := genTestCommand("sub1sub3", true)
 | 
						s1_3 := genTestCommand("sub1sub3", true)
 | 
				
			||||||
	s1_3.Flags().IntVar(&in1, "int1", in1, "int1 descriptionn")
 | 
						s1_3.Flags().IntVar(&in1, "int1", in1, "int1 descriptionn")
 | 
				
			||||||
	s1_3.Flags().IntVar(&in2, "int2", in2, "int2 descriptionn")
 | 
						s1_3.Flags().IntVar(&in2, "int2", in2, "int2 descriptionn")
 | 
				
			||||||
 | 
						s1_3.Flags().StringArrayP("option", "O", []string{}, "more options")
 | 
				
			||||||
	s2_1 := genTestCommand("sub2sub1", true)
 | 
						s2_1 := genTestCommand("sub2sub1", true)
 | 
				
			||||||
	s2_2 := genTestCommand("sub2sub2", true)
 | 
						s2_2 := genTestCommand("sub2sub2", true)
 | 
				
			||||||
	s2_3 := genTestCommand("sub2sub3", true)
 | 
						s2_3 := genTestCommand("sub2sub3", true)
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user