Use the empty string as a parameter to the flag being completed

Also attempt to standardize the bash and zsh implementations

Next up: tests!
This commit is contained in:
PapaCharlie 2019-06-20 13:41:21 -07:00
parent 53e4e28648
commit ee1287cc3f
4 changed files with 55 additions and 44 deletions

View file

@ -280,7 +280,12 @@ __%[1]s_handle_word()
__%[1]s_handle_word __%[1]s_handle_word
} }
__%[1]s_handle_dynamic_flag_completion() `, name))
}
func writeDynamicFlagCompletionFunction(buf *bytes.Buffer, dynamicFlagCompletionFunc string) {
buf.WriteString(fmt.Sprintf(`
%s()
{ {
export COBRA_FLAG_COMPLETION="$1" export COBRA_FLAG_COMPLETION="$1"
@ -289,7 +294,7 @@ __%[1]s_handle_dynamic_flag_completion()
return $? return $?
fi fi
if ! error_message="$($COMP_LINE > "$output")" ; then if ! error_message="$("${COMP_WORDS[@]}" > "$output")" ; then
local st="$?" local st="$?"
echo "$error_message" echo "$error_message"
return "$st" return "$st"
@ -303,7 +308,7 @@ __%[1]s_handle_dynamic_flag_completion()
rm "$output" rm "$output"
} }
`, name)) `, dynamicFlagCompletionFunc))
} }
func writePostscript(buf *bytes.Buffer, name string) { func writePostscript(buf *bytes.Buffer, name string) {
@ -553,17 +558,19 @@ func gen(buf *bytes.Buffer, cmd *Command) {
// GenBashCompletion generates bash completion file and writes to the passed writer. // GenBashCompletion generates bash completion file and writes to the passed writer.
func (c *Command) GenBashCompletion(w io.Writer) error { func (c *Command) GenBashCompletion(w io.Writer) error {
visitAllFlagsWithCompletions(c, func(f *pflag.Flag) { dynamicFlagCompletionFunc := "__" + c.Root().Name() + "_handle_dynamic_flag_completion"
if f.Annotations == nil { c.Root().visitAllFlagsWithCompletions(func(f *pflag.Flag) {
f.Annotations = make(map[string][]string) f.Annotations[BashCompDynamic] = []string{dynamicFlagCompletionFunc + " " + f.Name}
}
f.Annotations[BashCompDynamic] = []string{"__" + c.Root().Name() + "_handle_dynamic_flag_completion " + f.Name}
}) })
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
writePreamble(buf, c.Name()) writePreamble(buf, c.Name())
if len(c.BashCompletionFunction) > 0 { if len(c.BashCompletionFunction) > 0 {
buf.WriteString(c.BashCompletionFunction + "\n") buf.WriteString(c.BashCompletionFunction + "\n")
} }
if c.HasDynamicCompletions() {
writeDynamicFlagCompletionFunction(buf, dynamicFlagCompletionFunc)
}
gen(buf, c) gen(buf, c)
writePostscript(buf, c.Name()) writePostscript(buf, c.Name())

View file

@ -212,9 +212,9 @@ type Command struct {
// errWriter is a writer defined by the user that replaces stderr // errWriter is a writer defined by the user that replaces stderr
errWriter io.Writer errWriter io.Writer
// flagCompletions is a map of flag to a function that returns a list of values to suggest during tab completion for // dynamicFlagCompletions is a map of flag to a function that returns a list of values to suggest during tab
// this flag // completion for this flag
flagCompletions map[*flag.Flag]DynamicFlagCompletion dynamicFlagCompletions map[*flag.Flag]DynamicFlagCompletion
} }
// Context returns underlying command context. If command wasn't // Context returns underlying command context. If command wasn't
@ -907,7 +907,6 @@ func (c *Command) complete(flagName string, a []string) (err error) {
} else { } else {
c.Flags().StringVar(&currentCompletionValue, flagName, "", "") c.Flags().StringVar(&currentCompletionValue, flagName, "", "")
} }
c.Flag(flagName).NoOptDefVal = "_hack_"
err = c.ParseFlags(a) err = c.ParseFlags(a)
if err != nil { if err != nil {
@ -917,10 +916,10 @@ func (c *Command) complete(flagName string, a []string) (err error) {
c.preRun() c.preRun()
currentCommand := c currentCommand := c
completionFunc := currentCommand.flagCompletions[flagToComplete] completionFunc := currentCommand.dynamicFlagCompletions[flagToComplete]
for completionFunc == nil && currentCommand.HasParent() { for completionFunc == nil && currentCommand.HasParent() {
currentCommand = currentCommand.Parent() currentCommand = currentCommand.Parent()
completionFunc = currentCommand.flagCompletions[flagToComplete] completionFunc = currentCommand.dynamicFlagCompletions[flagToComplete]
} }
if completionFunc == nil { if completionFunc == nil {
return fmt.Errorf("%s does not have completions enabled", flagName) return fmt.Errorf("%s does not have completions enabled", flagName)
@ -958,6 +957,7 @@ func (c *Command) complete(flagName string, a []string) (err error) {
} }
for _, v := range values { for _, v := range values {
c.OutOrStdout()
fmt.Print(v + "\x00") fmt.Print(v + "\x00")
} }
@ -1741,15 +1741,26 @@ func (c *Command) updateParentsPflags() {
}) })
} }
func visitAllFlagsWithCompletions(c *Command, fn func(*flag.Flag)) { func (c *Command) HasDynamicCompletions() bool {
hasCompletions := false
c.visitAllFlagsWithCompletions(func(*flag.Flag) { hasCompletions = true })
return hasCompletions
}
// visitAllFlagsWithCompletions recursively visits all flags and persistent flags that have dynamic completions enabled.
// Initializes the flag's Annotations map if nil before calling fn
func (c Command) visitAllFlagsWithCompletions(fn func(*flag.Flag)) {
filterFunc := func(f *flag.Flag) { filterFunc := func(f *flag.Flag) {
if _, ok := c.flagCompletions[f]; ok { if _, ok := c.dynamicFlagCompletions[f]; ok {
if f.Annotations == nil {
f.Annotations = make(map[string][]string)
}
fn(f) fn(f)
} }
} }
c.Flags().VisitAll(filterFunc) c.Flags().VisitAll(filterFunc)
c.PersistentFlags().VisitAll(filterFunc) c.PersistentFlags().VisitAll(filterFunc)
for _, sc := range c.Commands() { for _, sc := range c.Commands() {
visitAllFlagsWithCompletions(sc, fn) sc.visitAllFlagsWithCompletions(fn)
} }
} }

View file

@ -103,10 +103,10 @@ func (c *Command) MarkDynamicFlagCompletion(name string, completion DynamicFlagC
return fmt.Errorf("%s takes no parameters", name) return fmt.Errorf("%s takes no parameters", name)
} }
if c.flagCompletions == nil { if c.dynamicFlagCompletions == nil {
c.flagCompletions = make(map[*pflag.Flag]DynamicFlagCompletion) c.dynamicFlagCompletions = make(map[*pflag.Flag]DynamicFlagCompletion)
} }
c.flagCompletions[flag] = completion c.dynamicFlagCompletions[flag] = completion
return nil return nil
} }

View file

@ -27,7 +27,6 @@ var (
"genFlagEntryForZshArguments": zshCompGenFlagEntryForArguments, "genFlagEntryForZshArguments": zshCompGenFlagEntryForArguments,
"extractArgsCompletions": zshCompExtractArgumentCompletionHintsForRendering, "extractArgsCompletions": zshCompExtractArgumentCompletionHintsForRendering,
"genZshFlagDynamicCompletionFuncName": zshCompGenDynamicFlagCompletionFuncName, "genZshFlagDynamicCompletionFuncName": zshCompGenDynamicFlagCompletionFuncName,
"hasDynamicCompletions": zshCompHasDynamicCompletions,
} }
zshCompletionText = ` zshCompletionText = `
{{/* should accept Command (that contains subcommands) as parameter */}} {{/* should accept Command (that contains subcommands) as parameter */}}
@ -35,6 +34,9 @@ var (
{{ $cmdPath := genZshFuncName .}} {{ $cmdPath := genZshFuncName .}}
function {{$cmdPath}} { function {{$cmdPath}} {
local -a commands local -a commands
{{/* If we are at the root, save a copy of the $words array as it contains the full command, including any empty
strings and other parameters */}}
{{ if (not .HasParent) and .HasDynamicCompletions }} full_command=("${(@)words}"){{- end}}
_arguments -C \{{- range extractFlags .}} _arguments -C \{{- range extractFlags .}}
{{genFlagEntryForZshArguments .}} \{{- end}} {{genFlagEntryForZshArguments .}} \{{- end}}
@ -82,7 +84,7 @@ function {{genZshFuncName .}} {
{{define "Main" -}} {{define "Main" -}}
#compdef _{{.Name}} {{.Name}} #compdef _{{.Name}} {{.Name}}
{{if hasDynamicCompletions . -}} {{if .HasDynamicCompletions -}}
function {{genZshFlagDynamicCompletionFuncName .}} { function {{genZshFlagDynamicCompletionFuncName .}} {
export COBRA_FLAG_COMPLETION="$1" export COBRA_FLAG_COMPLETION="$1"
@ -91,7 +93,7 @@ function {{genZshFlagDynamicCompletionFuncName .}} {
return $? return $?
fi fi
if ! error_message="$($tokens 2>&1 > "$output")" ; then if ! error_message="$("${(@)full_command}" 2>&1 > "$output")" ; then
local st="$?" local st="$?"
_message "Exception occurred during completion: $error_message" _message "Exception occurred during completion: $error_message"
return "$st" return "$st"
@ -102,7 +104,11 @@ function {{genZshFlagDynamicCompletionFuncName .}} {
args+="$line" args+="$line"
done < "$output" done < "$output"
_values "$1" $args if [[ $#args -gt 0 ]] ; then
_values "$1" "${(@)args}"
else
_message "No matching completion for $descr: $opt_args"
fi
unset COBRA_FLAG_COMPLETION unset COBRA_FLAG_COMPLETION
rm "$output" rm "$output"
@ -141,12 +147,11 @@ func (c *Command) GenZshCompletionFile(filename string) error {
// writer. The completion always run on the root command regardless of the // writer. The completion always run on the root command regardless of the
// command it was called from. // command it was called from.
func (c *Command) GenZshCompletion(w io.Writer) error { func (c *Command) GenZshCompletion(w io.Writer) error {
visitAllFlagsWithCompletions(c, func(f *pflag.Flag) { dynamicFlagCompletionFuncName := zshCompGenDynamicFlagCompletionFuncName(c.Root())
if f.Annotations == nil { c.Root().visitAllFlagsWithCompletions(func(f *pflag.Flag) {
f.Annotations = make(map[string][]string) f.Annotations[zshCompDynamicCompletion] = []string{dynamicFlagCompletionFuncName + " " + f.Name}
}
f.Annotations[zshCompDynamicCompletion] = []string{zshCompGenDynamicFlagCompletionFuncName(c)}
}) })
tmpl, err := template.New("Main").Funcs(zshCompFuncMap).Parse(zshCompletionText) tmpl, err := template.New("Main").Funcs(zshCompFuncMap).Parse(zshCompletionText)
if err != nil { if err != nil {
return fmt.Errorf("error creating zsh completion template: %v", err) return fmt.Errorf("error creating zsh completion template: %v", err)
@ -285,10 +290,6 @@ func zshCompGenFuncName(c *Command) string {
return "_" + c.Name() return "_" + c.Name()
} }
func zshCompGenDynamicFlagCompletionFuncName(c *Command) string {
return "_" + c.Root().Name() + "-flag-completion"
}
func zshCompExtractFlag(c *Command) []*pflag.Flag { func zshCompExtractFlag(c *Command) []*pflag.Flag {
var flags []*pflag.Flag var flags []*pflag.Flag
c.LocalFlags().VisitAll(func(f *pflag.Flag) { c.LocalFlags().VisitAll(func(f *pflag.Flag) {
@ -360,7 +361,7 @@ func zshCompGenFlagEntryExtras(f *pflag.Flag) string {
extras = extras + fmt.Sprintf(` -g "%s"`, pattern) extras = extras + fmt.Sprintf(` -g "%s"`, pattern)
} }
case zshCompDynamicCompletion: case zshCompDynamicCompletion:
extras += fmt.Sprintf(":{%s %s}", values[0], f.Name) extras += fmt.Sprintf(":{%s}", values[0])
} }
} }
@ -376,14 +377,6 @@ func zshCompQuoteFlagDescription(s string) string {
return strings.Replace(s, "'", `'\''`, -1) return strings.Replace(s, "'", `'\''`, -1)
} }
func zshCompHasDynamicCompletions(c *Command) bool { func zshCompGenDynamicFlagCompletionFuncName(c *Command) string {
if len(c.flagCompletions) > 0 { return "_" + c.Root().Name() + "-handle-dynamic-flag-completion"
return true
}
for _, subcommand := range c.Commands() {
if zshCompHasDynamicCompletions(subcommand) {
return true
}
}
return false
} }