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:
Haim Ashkenazi 2018-02-28 17:04:17 +02:00 committed by Steve Francia
parent e8018e8612
commit ec4b8c974c
2 changed files with 85 additions and 25 deletions

View file

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"strings"
"text/template" "text/template"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@ -11,26 +12,11 @@ import (
var ( 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
}

View file

@ -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)