Implement bash completions!

This commit is contained in:
PapaCharlie 2019-06-19 14:22:09 -07:00
parent 684fd47788
commit 979059b9c4
4 changed files with 81 additions and 17 deletions

View file

@ -15,6 +15,7 @@ import (
const ( const (
BashCompFilenameExt = "cobra_annotation_bash_completion_filename_extensions" BashCompFilenameExt = "cobra_annotation_bash_completion_filename_extensions"
BashCompCustom = "cobra_annotation_bash_completion_custom" BashCompCustom = "cobra_annotation_bash_completion_custom"
BashCompDynamic = "cobra_annotation_bash_completion_dynamic"
BashCompOneRequiredFlag = "cobra_annotation_bash_completion_one_required_flag" BashCompOneRequiredFlag = "cobra_annotation_bash_completion_one_required_flag"
BashCompSubdirsInDir = "cobra_annotation_bash_completion_subdirs_in_dir" BashCompSubdirsInDir = "cobra_annotation_bash_completion_subdirs_in_dir"
) )
@ -279,6 +280,29 @@ __%[1]s_handle_word()
__%[1]s_handle_word __%[1]s_handle_word
} }
__%[1]s_handle_dynamic_flag_completion()
{
export COBRA_FLAG_COMPLETION="$1"
local output
if ! output="$(mktemp)" ; then
return $?
fi
if ! error_message="$($COMP_LINE > "$output")" ; then
local st="$?"
echo "$error_message"
return "$st"
fi
while read -r -d '' line ; do
COMPREPLY+=("$line")
done < "$output"
unset COBRA_FLAG_COMPLETION
rm "$output"
}
`, name)) `, name))
} }
@ -354,6 +378,9 @@ func writeFlagHandler(buf *bytes.Buffer, name string, annotations map[string][]s
} else { } else {
buf.WriteString(" flags_completion+=(:)\n") buf.WriteString(" flags_completion+=(:)\n")
} }
case BashCompDynamic:
buf.WriteString(fmt.Sprintf(" flags_with_completion+=(%q)\n", name))
buf.WriteString(fmt.Sprintf(" flags_completion+=(%q)\n", value[0]))
case BashCompSubdirsInDir: case BashCompSubdirsInDir:
buf.WriteString(fmt.Sprintf(" flags_with_completion+=(%q)\n", name)) buf.WriteString(fmt.Sprintf(" flags_with_completion+=(%q)\n", name))
@ -526,6 +553,12 @@ 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) {
if f.Annotations == nil {
f.Annotations = make(map[string][]string)
}
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 {

View file

@ -21,6 +21,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"log"
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
@ -897,6 +898,10 @@ func (c *Command) complete(flagName string, a []string) (err error) {
c.Flags().AddFlag(f) c.Flags().AddFlag(f)
} }
}) })
if flagToComplete == nil {
log.Panicln(flagName, "is not a known flag")
}
if flagToComplete.Shorthand != "" { if flagToComplete.Shorthand != "" {
c.Flags().StringVarP(&currentCompletionValue, flagName, flagToComplete.Shorthand, "", "") c.Flags().StringVarP(&currentCompletionValue, flagName, flagToComplete.Shorthand, "", "")
} else { } else {
@ -953,7 +958,7 @@ func (c *Command) complete(flagName string, a []string) (err error) {
} }
for _, v := range values { for _, v := range values {
c.Print(v + "\x00") fmt.Print(v + "\x00")
} }
return nil return nil
@ -1735,3 +1740,16 @@ func (c *Command) updateParentsPflags() {
c.parentsPflags.AddFlagSet(parent.PersistentFlags()) c.parentsPflags.AddFlagSet(parent.PersistentFlags())
}) })
} }
func visitAllFlagsWithCompletions(c *Command, fn func(*flag.Flag)) {
filterFunc := func(f *flag.Flag) {
if _, ok := c.flagCompletions[f]; ok {
fn(f)
}
}
c.Flags().VisitAll(filterFunc)
c.PersistentFlags().VisitAll(filterFunc)
for _, sc := range c.Commands() {
visitAllFlagsWithCompletions(sc, fn)
}
}

View file

@ -93,7 +93,7 @@ type DynamicFlagCompletion func(currentValue string) (suggestedValues []string,
// RunPreRunsDuringCompletion is set to true. All flags other than the flag currently being completed will be parsed // RunPreRunsDuringCompletion is set to true. All flags other than the flag currently being completed will be parsed
// according to their type. The flag being completed is parsed as a raw string with no format requirements // according to their type. The flag being completed is parsed as a raw string with no format requirements
// //
// Shell Completion compatibility matrix: zsh // Shell Completion compatibility matrix: bash, zsh
func (c *Command) MarkDynamicFlagCompletion(name string, completion DynamicFlagCompletion) error { func (c *Command) MarkDynamicFlagCompletion(name string, completion DynamicFlagCompletion) error {
flag := c.Flag(name) flag := c.Flag(name)
if flag == nil { if flag == nil {
@ -107,9 +107,6 @@ func (c *Command) MarkDynamicFlagCompletion(name string, completion DynamicFlagC
c.flagCompletions = make(map[*pflag.Flag]DynamicFlagCompletion) c.flagCompletions = make(map[*pflag.Flag]DynamicFlagCompletion)
} }
c.flagCompletions[flag] = completion c.flagCompletions[flag] = completion
if flag.Annotations == nil {
flag.Annotations = map[string][]string{}
}
flag.Annotations[zshCompDynamicCompletion] = []string{zshCompGenFlagCompletionFuncName(c)}
return nil return nil
} }

View file

@ -26,9 +26,8 @@ var (
"extractFlags": zshCompExtractFlag, "extractFlags": zshCompExtractFlag,
"genFlagEntryForZshArguments": zshCompGenFlagEntryForArguments, "genFlagEntryForZshArguments": zshCompGenFlagEntryForArguments,
"extractArgsCompletions": zshCompExtractArgumentCompletionHintsForRendering, "extractArgsCompletions": zshCompExtractArgumentCompletionHintsForRendering,
"genZshFlagDynamicCompletionFuncName": zshCompGenFlagCompletionFuncName, "genZshFlagDynamicCompletionFuncName": zshCompGenDynamicFlagCompletionFuncName,
"hasDynamicCompletions": zshCompHasDynamicCompletions, "hasDynamicCompletions": zshCompHasDynamicCompletions,
"flagCompletionsEnvVar": func() string { return FlagCompletionEnvVar },
} }
zshCompletionText = ` zshCompletionText = `
{{/* should accept Command (that contains subcommands) as parameter */}} {{/* should accept Command (that contains subcommands) as parameter */}}
@ -86,16 +85,27 @@ function {{genZshFuncName .}} {
{{if hasDynamicCompletions . -}} {{if hasDynamicCompletions . -}}
function {{genZshFlagDynamicCompletionFuncName .}} { function {{genZshFlagDynamicCompletionFuncName .}} {
export COBRA_FLAG_COMPLETION="$1" export COBRA_FLAG_COMPLETION="$1"
if suggestions="$("$words[@]" 2>&1)" ; then
local -a args local output
while read -d $'\0' line ; do if ! output="$(mktemp)" ; then
args+="$line" return $?
done <<< "$suggestions"
_values "$1" "$args[@]"
else
_message "Exception occurred during completion: $suggestions"
fi fi
if ! error_message="$("${tokens[@]}" 2>&1 > "$output")" ; then
local st="$?"
_message "Exception occurred during completion: $error_message"
return "$st"
fi
local -a args
while read -r -d '' line ; do
args+="$line"
done < "$output"
_values "$1" "$args[@]"
unset COBRA_FLAG_COMPLETION unset COBRA_FLAG_COMPLETION
rm "$output"
}{{- end}} }{{- end}}
{{template "selectCmdTemplate" .}} {{template "selectCmdTemplate" .}}
@ -131,6 +141,12 @@ 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) {
if f.Annotations == nil {
f.Annotations = make(map[string][]string)
}
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)
@ -269,7 +285,7 @@ func zshCompGenFuncName(c *Command) string {
return "_" + c.Name() return "_" + c.Name()
} }
func zshCompGenFlagCompletionFuncName(c *Command) string { func zshCompGenDynamicFlagCompletionFuncName(c *Command) string {
return "_" + c.Root().Name() + "-flag-completion" return "_" + c.Root().Name() + "-flag-completion"
} }