From 913b78e7a90365f925f803f6dd8d0033abd42e3b Mon Sep 17 00:00:00 2001 From: Eli Young Date: Thu, 2 Jun 2016 17:01:39 -0700 Subject: [PATCH] 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. --- bash_completions.go | 59 ++++++++++++++++++++++++++++++++++++++++ bash_completions.md | 15 ++++++++++ bash_completions_test.go | 13 +++++++++ 3 files changed, 87 insertions(+) diff --git a/bash_completions.go b/bash_completions.go index 3f33bb0e..41e6dbbf 100644 --- a/bash_completions.go +++ b/bash_completions.go @@ -14,6 +14,7 @@ const ( BashCompFilenameExt = "cobra_annotation_bash_completion_filename_extentions" BashCompCustom = "cobra_annotation_bash_completion_custom" BashCompOneRequiredFlag = "cobra_annotation_bash_completion_one_required_flag" + BashCompFlagIsCommand = "cobra_annocation_bash_completion_flag_is_command" BashCompSubdirsInDir = "cobra_annotation_bash_completion_subdirs_in_dir" ) @@ -122,6 +123,7 @@ __handle_reply() completions=("${must_have_one_noun[@]}") else completions=("${commands[@]}") + completions+=("${flag_command_substitutes[@]}") fi COMPREPLY=( $(compgen -W "${completions[*]}" -- "$cur") ) @@ -167,6 +169,12 @@ __handle_flag() must_have_one_flag=() 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 if [ -n "${flagvalue}" ] ; then flaghash[${flagname}]=${flagvalue} @@ -466,6 +474,39 @@ func writeRequiredFlag(cmd *Command, w io.Writer) error { 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 { if _, err := fmt.Fprintf(w, " must_have_one_noun=()\n"); err != nil { return err @@ -519,6 +560,9 @@ func gen(cmd *Command, w io.Writer) error { if err := writeRequiredFlag(cmd, w); err != nil { return err } + if err := writeFlagCommandSubstitutes(cmd, w); err != nil { + return err + } if err := writeRequiredNouns(cmd, w); err != nil { return err } @@ -571,6 +615,21 @@ func MarkFlagRequired(flags *pflag.FlagSet, name string) error { 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. // Generated bash autocompletion will select filenames for the flag, limiting to named extensions if provided. func (cmd *Command) MarkFlagFilename(name string, extensions ...string) error { diff --git a/bash_completions.md b/bash_completions.md index 84d5b012..83f5777d 100644 --- a/bash_completions.md +++ b/bash_completions.md @@ -143,6 +143,21 @@ and you'll get something like -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 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. diff --git a/bash_completions_test.go b/bash_completions_test.go index 6957f8bd..369a795b 100644 --- a/bash_completions_test.go +++ b/bash_completions_test.go @@ -97,6 +97,16 @@ func TestBashCompletions(t *testing.T) { c.Flags().StringVar(&flagvalTheme, "theme", "", "theme to use (located in /themes/THEMENAME/)") 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) c.GenBashCompletion(out) str := out.String() @@ -110,6 +120,9 @@ func TestBashCompletions(t *testing.T) { // check for required flags check(t, str, `must_have_one_flag+=("--introot=")`) 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(t, str, `COMPREPLY=( "hello" )`) // check for required nouns