Add Bash support for command-substitute flags

These are useful to mark a flag as taking the place of a command. For
example, `kubectl create` can be called using either a subcommand or by
providing a file as input:

    kubectl create --filename=file-input.yml

    kubectl create secret generic [...]

This allows the generated Bash completions to reflect this and suggest
both commands and command-substitute flags.
This commit is contained in:
Eli Young 2016-06-02 17:01:39 -07:00
parent f368244301
commit 913b78e7a9
3 changed files with 87 additions and 0 deletions

View file

@ -14,6 +14,7 @@ const (
BashCompFilenameExt = "cobra_annotation_bash_completion_filename_extentions" BashCompFilenameExt = "cobra_annotation_bash_completion_filename_extentions"
BashCompCustom = "cobra_annotation_bash_completion_custom" BashCompCustom = "cobra_annotation_bash_completion_custom"
BashCompOneRequiredFlag = "cobra_annotation_bash_completion_one_required_flag" BashCompOneRequiredFlag = "cobra_annotation_bash_completion_one_required_flag"
BashCompFlagIsCommand = "cobra_annocation_bash_completion_flag_is_command"
BashCompSubdirsInDir = "cobra_annotation_bash_completion_subdirs_in_dir" BashCompSubdirsInDir = "cobra_annotation_bash_completion_subdirs_in_dir"
) )
@ -122,6 +123,7 @@ __handle_reply()
completions=("${must_have_one_noun[@]}") completions=("${must_have_one_noun[@]}")
else else
completions=("${commands[@]}") completions=("${commands[@]}")
completions+=("${flag_command_substitutes[@]}")
fi fi
COMPREPLY=( $(compgen -W "${completions[*]}" -- "$cur") ) COMPREPLY=( $(compgen -W "${completions[*]}" -- "$cur") )
@ -167,6 +169,12 @@ __handle_flag()
must_have_one_flag=() must_have_one_flag=()
fi fi
# if the flag is a substitute for a command, unset commands() and flag_command_substitutes()
if __contains_word "${flagname}" "${flag_command_substitutes[@]}"; then
commands=()
flag_command_substitutes=()
fi
# keep flag value with flagname as flaghash # keep flag value with flagname as flaghash
if [ -n "${flagvalue}" ] ; then if [ -n "${flagvalue}" ] ; then
flaghash[${flagname}]=${flagvalue} flaghash[${flagname}]=${flagvalue}
@ -466,6 +474,39 @@ func writeRequiredFlag(cmd *Command, w io.Writer) error {
return visitErr return visitErr
} }
func writeFlagCommandSubstitutes(cmd *Command, w io.Writer) error {
if _, err := fmt.Fprintf(w, " flag_command_substitutes=()\n"); err != nil {
return err
}
flags := cmd.NonInheritedFlags()
var visitErr error
flags.VisitAll(func(flag *pflag.Flag) {
for key := range flag.Annotations {
switch key {
case BashCompFlagIsCommand:
format := " flag_command_substitutes+=(\"--%s"
b := (flag.Value.Type() == "bool")
if !b {
format += "="
}
format += "\")\n"
if _, err := fmt.Fprintf(w, format, flag.Name); err != nil {
visitErr = err
return
}
if len(flag.Shorthand) > 0 {
if _, err := fmt.Fprintf(w, " flag_command_substitutes+=(\"-%s\")\n", flag.Shorthand); err != nil {
visitErr = err
return
}
}
}
}
})
return visitErr
}
func writeRequiredNouns(cmd *Command, w io.Writer) error { func writeRequiredNouns(cmd *Command, w io.Writer) error {
if _, err := fmt.Fprintf(w, " must_have_one_noun=()\n"); err != nil { if _, err := fmt.Fprintf(w, " must_have_one_noun=()\n"); err != nil {
return err return err
@ -519,6 +560,9 @@ func gen(cmd *Command, w io.Writer) error {
if err := writeRequiredFlag(cmd, w); err != nil { if err := writeRequiredFlag(cmd, w); err != nil {
return err return err
} }
if err := writeFlagCommandSubstitutes(cmd, w); err != nil {
return err
}
if err := writeRequiredNouns(cmd, w); err != nil { if err := writeRequiredNouns(cmd, w); err != nil {
return err return err
} }
@ -571,6 +615,21 @@ func MarkFlagRequired(flags *pflag.FlagSet, name string) error {
return flags.SetAnnotation(name, BashCompOneRequiredFlag, []string{"true"}) return flags.SetAnnotation(name, BashCompOneRequiredFlag, []string{"true"})
} }
// MarkFlagCommandSubstitute adds the BashCompFlagIsCommand annotation to the named flag, if it exists.
func (cmd *Command) MarkFlagCommandSubstitute(name string) error {
return MarkFlagCommandSubstitute(cmd.Flags(), name)
}
// MarkPersistentFlagCommandSubstitute adds the BashCompFlagIsCommand annotation to the named flag, if it exists.
func (cmd *Command) MarkPersistentFlagCommandSubstitute(name string) error {
return MarkFlagCommandSubstitute(cmd.PersistentFlags(), name)
}
// MarkFlagCommandSubstitute adds the BashCompFlagIsCommand annotation to the named flag in the flag set, if it exists.
func MarkFlagCommandSubstitute(flags *pflag.FlagSet, name string) error {
return flags.SetAnnotation(name, BashCompFlagIsCommand, []string{"true"})
}
// MarkFlagFilename adds the BashCompFilenameExt annotation to the named flag, if it exists. // MarkFlagFilename adds the BashCompFilenameExt annotation to the named flag, if it exists.
// Generated bash autocompletion will select filenames for the flag, limiting to named extensions if provided. // Generated bash autocompletion will select filenames for the flag, limiting to named extensions if provided.
func (cmd *Command) MarkFlagFilename(name string, extensions ...string) error { func (cmd *Command) MarkFlagFilename(name string, extensions ...string) error {

View file

@ -143,6 +143,21 @@ and you'll get something like
-c --container= -p --pod= -c --container= -p --pod=
``` ```
## Mark flags as substitutes for subcommands
A flag may be sufficient input to a command that would normally require subcommands. Most commonly, this is the case when the flag specifies a file containing operations to run. In this case, we mark the flag as a command substitute:
```go
cmd.MarkFlagCommandSubstitute("filename")
```
This will result in something like:
```bash
# kubectl create [tab][tab]
--filename= -f secret user
```
# Specify valid filename extensions for flags that take a filename # Specify valid filename extensions for flags that take a filename
In this example we use --filename= and expect to get a json or yaml file as the argument. To make this easier we annotate the --filename flag with valid filename extensions. In this example we use --filename= and expect to get a json or yaml file as the argument. To make this easier we annotate the --filename flag with valid filename extensions.

View file

@ -97,6 +97,16 @@ func TestBashCompletions(t *testing.T) {
c.Flags().StringVar(&flagvalTheme, "theme", "", "theme to use (located in /themes/THEMENAME/)") c.Flags().StringVar(&flagvalTheme, "theme", "", "theme to use (located in /themes/THEMENAME/)")
c.Flags().SetAnnotation("theme", BashCompSubdirsInDir, []string{"themes"}) c.Flags().SetAnnotation("theme", BashCompSubdirsInDir, []string{"themes"})
// command-substitute flag
var flagvalCommand string
c.Flags().StringVar(&flagvalCommand, "commandflag", "", "This is a command")
c.MarkFlagCommandSubstitute("commandflag")
// persistent command-substitute flag
var flagvalPersistentCommand string
c.PersistentFlags().StringVar(&flagvalPersistentCommand, "persistent-commandflag", "", "This is a command")
c.MarkPersistentFlagCommandSubstitute("persistent-commandflag")
out := new(bytes.Buffer) out := new(bytes.Buffer)
c.GenBashCompletion(out) c.GenBashCompletion(out)
str := out.String() str := out.String()
@ -110,6 +120,9 @@ func TestBashCompletions(t *testing.T) {
// check for required flags // check for required flags
check(t, str, `must_have_one_flag+=("--introot=")`) check(t, str, `must_have_one_flag+=("--introot=")`)
check(t, str, `must_have_one_flag+=("--persistent-filename=")`) check(t, str, `must_have_one_flag+=("--persistent-filename=")`)
// check for command-substitute flags
check(t, str, `flag_command_substitutes+=("--commandflag=")`)
check(t, str, `flag_command_substitutes+=("--persistent-commandflag=")`)
// check for custom completion function // check for custom completion function
check(t, str, `COMPREPLY=( "hello" )`) check(t, str, `COMPREPLY=( "hello" )`)
// check for required nouns // check for required nouns