diff --git a/active_help.go b/active_help.go index b3efa4875..0c631913d 100644 --- a/active_help.go +++ b/active_help.go @@ -26,27 +26,24 @@ func AppendActiveHelp(compArray []string, activeHelpStr string) []string { return append(compArray, fmt.Sprintf("%s%s", activeHelpMarker, activeHelpStr)) } +// GetActiveHelpConfig returns the value of the ActiveHelp environment variable +// _ACTIVE_HELP where is the name of the root command in upper +// case, with all - replaced by _. +// It will always return "0" if the global environment variable COBRA_ACTIVE_HELP +// is set to "0". +func GetActiveHelpConfig(cmd *Command) string { + activeHelpCfg := os.Getenv(activeHelpGlobalEnvVar) + if activeHelpCfg != activeHelpGlobalDisable { + activeHelpCfg = os.Getenv(activeHelpEnvVar(cmd.Root().Name())) + } + return activeHelpCfg +} + // activeHelpEnvVar returns the name of the program-specific ActiveHelp environment // variable. It has the format _ACTIVE_HELP where is the name of the // root command in upper case, with all - replaced by _. -// This format should not be changed: users will be using it explicitly. func activeHelpEnvVar(name string) string { + // This format should not be changed: users will be using it explicitly. activeHelpEnvVar := strings.ToUpper(fmt.Sprintf("%s%s", name, activeHelpEnvVarSuffix)) return strings.ReplaceAll(activeHelpEnvVar, "-", "_") } - -// setActiveHelpConfig first checks the global environment variable -// of ActiveHelp to see if it is disabling active help, and if it is not, -// it then looks to the program-specific variable. -// It then sets the ActiveHelpConfig value to make it available when -// calling the completion function. We also set it on the root, -// just in case users try to access it from there. -func setActiveHelpConfig(cmd *Command) { - activeHelpCfg := os.Getenv(activeHelpGlobalEnvVar) - if activeHelpCfg != activeHelpGlobalDisable { - activeHelpCfg = os.Getenv(activeHelpEnvVar(cmd.Root().Name())) - } - - cmd.ActiveHelpConfig = activeHelpCfg - cmd.Root().ActiveHelpConfig = cmd.ActiveHelpConfig -} diff --git a/active_help.md b/active_help.md index fb2d19074..5e7f59af3 100644 --- a/active_help.md +++ b/active_help.md @@ -13,6 +13,8 @@ Please specify the path to the chart to package bash-5.1$ bin/helm package [tab][tab] bin/ internal/ scripts/ pkg/ testdata/ ``` + +**Hint**: A good place to use Active Help messages is when the normal completion system does not provide any suggestions. In such cases, Active Help nicely supplements the normal shell completions to guide the user in knowing what is expected by the program. ## Supported shells Active Help is currently only supported for the following shells: @@ -61,6 +63,7 @@ You must specify the URL for the repo you are adding bash-5.1$ helm repo add grafana https://grafana.github.io/helm-charts [tab] This command does not take any more arguments ``` +**Hint**: As can be seen in the above example, a good place to use Active Help messages is when the normal completion system does not provide any suggestions. In such cases, Active Help nicely supplements the normal shell completions. ### Active Help for flags @@ -84,28 +87,26 @@ bash-5.1$ bin/helm install myrelease bitnami/solr --version 2.0.[tab][tab] ## User control of Active Help -You may want to allow your users to disable Active Help or choose between different levels of Active Help. It is entirely up to the program to define the type of configurability of Active Help that it wants to offer. - -### Configuration using an environment variable +You may want to allow your users to disable Active Help or choose between different levels of Active Help. It is entirely up to the program to define the type of configurability of Active Help that it wants to offer, if any. +Allowing to configure Active Help is entirely optional; you can use Active Help in your program without doing anything about Active Help configuration. -One way to configure Active Help is to use the program's Active Help environment +The way to configure Active Help is to use the program's Active Help environment variable. That variable is named `_ACTIVE_HELP` where `` is the name of your -program in uppercase with any `-` replaced by an `_`. You can find that variable in the generated -completion scripts of your program. The variable should be set by the user to whatever Active Help -configuration values are supported by the program. +program in uppercase with any `-` replaced by an `_`. The variable should be set by the user to whatever +Active Help configuration values are supported by the program. -For example, say `helm` supports three levels for Active Help: `on`, `off`, `local`. Then a user +For example, say `helm` has chosen to support three levels for Active Help: `on`, `off`, `local`. Then a user would set the desired behavior to `local` by doing `export HELM_ACTIVE_HELP=local` in their shell. -When in `cmd.ValidArgsFunction(...)` or a flag's completion function, the program should read the -Active Help configuration from the `cmd.ActiveHelpConfig` field and select what Active Help messages -should or should not be added. +For simplicity, when in `cmd.ValidArgsFunction(...)` or a flag's completion function, the program should read the +Active Help configuration using the `cobra.GetActiveHelpConfig(cmd)` function and select what Active Help messages +should or should not be added (instead of reading the environment variable directly). For example: ```go ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - activeHelpLevel := cmd.ActiveHelpConfig - + activeHelpLevel := cobra.GetActiveHelpConfig(cmd) + var comps []string if len(args) == 0 { if activeHelpLevel != "off" { @@ -123,49 +124,23 @@ ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([ return comps, cobra.ShellCompDirectiveNoFileComp }, ``` -**Note 1**: If the string "0" is used for `cmd.Root().ActiveHelpConfig`, it will automatically be handled by Cobra and will completely disable all Active Help output (even if some output was specified by the program using the `cobra.AppendActiveHelp(...)` function). Using "0" can simplify your code in situations where you want to blindly disable Active Help. - -**Note 2**: Cobra transparently passes the `cmd.ActiveHelpConfig` string you specified back to your program when completion is invoked. You can therefore define any scheme you choose for your program; you are not limited to using integer levels for the configuration of Active Help. **However, the reserved "0" value can also be sent to you program and you should be prepared for it.** - -**Note 3**: If a user wants to disable Active Help for every single program based on Cobra, the global environment variable `COBRA_ACTIVE_HELP` can be used as follows: -``` -export COBRA_ACTIVE_HELP=0 -``` - -### Configuration using a flag - -Another approach for a user to configure Active Help is for the program to add a flag to the command that generates -the completion script. Using the flag, the user specifies the Active Help configuration that is -desired. Then the program should specify that configuration by setting the `rootCmd.ActiveHelpConfig` string -before calling the Cobra API that generates the shell completion script. -The ActiveHelp configuration would then be read in `cmd.ValidArgsFunction(...)` or a flag's completion function, in the same -fashion as explained above, using the same `cmd.ActiveHelpConfig` field. - -For example, a program that uses a `completion` command to generate the shell completion script can add a flag `--activehelp-level` to that command. The user would then use that flag to choose an Active Help level: -``` -bash-5.1$ source <(helm completion bash --activehelp-level 1) -``` -The code to pass that information to Cobra would look something like: -```go -cmd.Root().ActiveHelpConfig = strconv.Itoa(activeHelpLevel) -return cmd.Root().GenBashCompletionV2(out, true) -``` +**Note 1**: If the `_ACTIVE_HELP` environment variable is set to the string "0", Cobra will automatically disable all Active Help output (even if some output was specified by the program using the `cobra.AppendActiveHelp(...)` function). Using "0" can simplify your code in situations where you want to blindly disable Active Help without having to call `cobra.GetActiveHelpConfig(cmd)` explicitly. -The advantage of using a flag is that it becomes self-documenting through Cobra's `help` command. Also, it allows you to -use Cobra's flag parsing to handle the configuration value, instead of having to deal with a environment variable natively. +**Note 2**: If a user wants to disable Active Help for every single program based on Cobra, she can set the environment variable `COBRA_ACTIVE_HELP` to "0". In this case `cobra.GetActiveHelpConfig(cmd)` will return "0" no matter what the variable `_ACTIVE_HELP` is set to. +**Note 3**: If the user does not set `_ACTIVE_HELP` or `COBRA_ACTIVE_HELP` (which will be a common case), the default value for the Active Help configuration returned by `cobra.GetActiveHelpConfig(cmd)` will be the empty string. ## Active Help with Cobra's default completion command Cobra provides a default `completion` command for programs that wish to use it. -When using the default `completion` command, Active Help is configurable using the -environment variable approach described above. You may wish to document this in more +When using the default `completion` command, Active Help is configurable in the same +fashion as described above using environment variables. You may wish to document this in more details for your users. ## Debugging Active Help -Debugging your Active Help code is done in the same way as debugging the dynamic completion code, which is with Cobra's hidden `__complete` command. Please refer to [debugging shell completion](shell_completions.md#debugging) for details. +Debugging your Active Help code is done in the same way as debugging your dynamic completion code, which is with Cobra's hidden `__complete` command. Please refer to [debugging shell completion](shell_completions.md#debugging) for details. -When debugging with the `__complete` command, if you want to specify different Active Help configurations, you should use the active help environment variable (as you can find in the generated completion scripts). That variable is named `_ACTIVE_HELP` where any `-` is replaced by an `_`. For example, we can test deactivating some Active Help as shown below: +When debugging with the `__complete` command, if you want to specify different Active Help configurations, you should use the active help environment variable. That variable is named `_ACTIVE_HELP` where any `-` is replaced by an `_`. For example, we can test deactivating some Active Help as shown below: ``` $ HELM_ACTIVE_HELP=1 bin/helm __complete install wordpress bitnami/h bitnami/haproxy diff --git a/active_help_test.go b/active_help_test.go index 28fc3b36c..524be884b 100644 --- a/active_help_test.go +++ b/active_help_test.go @@ -263,17 +263,13 @@ func TestConfigActiveHelp(t *testing.T) { rootCmd.AddCommand(childCmd) activeHelpCfg := "someconfig,anotherconfig" - // Set the variable that the completions scripts will be setting + // Set the variable that the user would be setting os.Setenv(activeHelpEnvVar(rootCmd.Name()), activeHelpCfg) childCmd.ValidArgsFunction = func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) { - // The activeHelpConfig variable should be set on the command - if cmd.ActiveHelpConfig != activeHelpCfg { - t.Errorf("expected activeHelpConfig on command: %q, but got: %q", activeHelpCfg, cmd.ActiveHelpConfig) - } - // The activeHelpConfig variable should also be set on the root - if cmd.Root().ActiveHelpConfig != activeHelpCfg { - t.Errorf("expected activeHelpConfig on root: %q, but got: %q", activeHelpCfg, cmd.Root().ActiveHelpConfig) + receivedActiveHelpCfg := GetActiveHelpConfig(cmd) + if receivedActiveHelpCfg != activeHelpCfg { + t.Errorf("expected activeHelpConfig: %q, but got: %q", activeHelpCfg, receivedActiveHelpCfg) } return nil, ShellCompDirectiveDefault } @@ -293,13 +289,9 @@ func TestConfigActiveHelp(t *testing.T) { // Test that multiple activeHelp message can be added _ = childCmd.RegisterFlagCompletionFunc(flagname, func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) { - // The activeHelpConfig variable should be set on the command - if cmd.ActiveHelpConfig != activeHelpCfg { - t.Errorf("expected activeHelpConfig on command: %q, but got: %q", activeHelpCfg, cmd.ActiveHelpConfig) - } - // The activeHelpConfig variable should also be set on the root - if cmd.Root().ActiveHelpConfig != activeHelpCfg { - t.Errorf("expected activeHelpConfig on root: %q, but got: %q", activeHelpCfg, cmd.Root().ActiveHelpConfig) + receivedActiveHelpCfg := GetActiveHelpConfig(cmd) + if receivedActiveHelpCfg != activeHelpCfg { + t.Errorf("expected activeHelpConfig: %q, but got: %q", activeHelpCfg, receivedActiveHelpCfg) } return nil, ShellCompDirectiveDefault }) @@ -380,13 +372,9 @@ func TestDisableActiveHelp(t *testing.T) { os.Setenv(activeHelpEnvVar(rootCmd.Name()), activeHelpCfg) childCmd.ValidArgsFunction = func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) { - // The activeHelpConfig variable should be set on the command - if cmd.ActiveHelpConfig != activeHelpCfg { - t.Errorf("expected activeHelpConfig on command: %q, but got: %q", activeHelpCfg, cmd.ActiveHelpConfig) - } - // The activeHelpConfig variable should also be set on the root - if cmd.Root().ActiveHelpConfig != activeHelpCfg { - t.Errorf("expected activeHelpConfig on root: %q, but got: %q", activeHelpCfg, cmd.Root().ActiveHelpConfig) + receivedActiveHelpCfg := GetActiveHelpConfig(cmd) + if receivedActiveHelpCfg != activeHelpCfg { + t.Errorf("expected activeHelpConfig: %q, but got: %q", activeHelpCfg, receivedActiveHelpCfg) } return nil, ShellCompDirectiveDefault } diff --git a/bash_completionsV2.go b/bash_completionsV2.go index 1506f22a8..d295791e6 100644 --- a/bash_completionsV2.go +++ b/bash_completionsV2.go @@ -9,12 +9,12 @@ import ( func (c *Command) genBashCompletion(w io.Writer, includeDesc bool) error { buf := new(bytes.Buffer) - genBashComp(buf, c, includeDesc) + genBashComp(buf, c.Name(), includeDesc) _, err := buf.WriteTo(w) return err } -func genBashComp(buf io.StringWriter, cmd *Command, includeDesc bool) { +func genBashComp(buf io.StringWriter, name string, includeDesc bool) { compCmd := ShellCompRequestCmd if !includeDesc { compCmd = ShellCompNoDescRequestCmd @@ -45,7 +45,7 @@ __%[1]s_get_completion_results() { # Prepare the command to request completions for the program. # Calling ${words[0]} instead of directly %[1]s allows to handle aliases args=("${words[@]:1}") - requestComp="%[9]s=${%[9]s-%[10]s} ${words[0]} %[2]s ${args[*]}" + requestComp="${words[0]} %[2]s ${args[*]}" lastParam=${words[$((${#words[@]}-1))]} lastChar=${lastParam:$((${#lastParam}-1)):1} @@ -345,10 +345,10 @@ else fi # ex: ts=4 sw=4 et filetype=sh -`, cmd.Name(), compCmd, +`, name, compCmd, ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, - activeHelpMarker, activeHelpEnvVar(cmd.Name()), cmd.Root().ActiveHelpConfig)) + activeHelpMarker)) } // GenBashCompletionFileV2 generates Bash completion version 2. diff --git a/command.go b/command.go index 65b2befb8..0f4511f38 100644 --- a/command.go +++ b/command.go @@ -222,23 +222,6 @@ type Command struct { // SuggestionsMinimumDistance defines minimum levenshtein distance to display suggestions. // Must be > 0. SuggestionsMinimumDistance int - - // ActiveHelpConfig is a string that can be used to communicate the level of activeHelp - // the user is interested in receiving. - // The program can set this string before generating the completion scripts. - // When setting this string, it MUST be set on the Root command. - // - // The program should read this string from within ValidArgsFunction or the flag value - // completion functions to make decisions on whether or not to append activeHelp messages. - // This string can be read directly from the command passed to the completion functions, - // or from the Root command. - // - // If the value 0 is used, it will automatically be handled by Cobra and - // will completely disable activeHelp output, even if some output was specified by - // the program. - // Any other value will not be interpreted by Cobra but only provided back - // to the program when ValidArgsFunction is called. - ActiveHelpConfig string } // Context returns underlying command context. If command was executed diff --git a/completions.go b/completions.go index 44d0ac0ca..4c7521b2d 100644 --- a/completions.go +++ b/completions.go @@ -178,7 +178,7 @@ func (c *Command) initCompleteCmd(args []string) { noDescriptions := (cmd.CalledAs() == ShellCompNoDescRequestCmd) for _, comp := range completions { - if finalCmd.ActiveHelpConfig == activeHelpGlobalDisable { + if GetActiveHelpConfig(finalCmd) == activeHelpGlobalDisable { // Remove all activeHelp entries in this case if strings.HasPrefix(comp, activeHelpMarker) { continue @@ -453,9 +453,6 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi // Go custom completion defined for this flag or command. // Call the registered completion function to get the completions. var comps []string - - setActiveHelpConfig(finalCmd) - comps, directive = completionFn(finalCmd, finalArgs, toComplete) completions = append(completions, comps...) } diff --git a/zsh_completions.go b/zsh_completions.go index 63a210765..65cd94c60 100644 --- a/zsh_completions.go +++ b/zsh_completions.go @@ -65,12 +65,12 @@ func (c *Command) genZshCompletionFile(filename string, includeDesc bool) error func (c *Command) genZshCompletion(w io.Writer, includeDesc bool) error { buf := new(bytes.Buffer) - genZshComp(buf, c, includeDesc) + genZshComp(buf, c.Name(), includeDesc) _, err := buf.WriteTo(w) return err } -func genZshComp(buf io.StringWriter, cmd *Command, includeDesc bool) { +func genZshComp(buf io.StringWriter, name string, includeDesc bool) { compCmd := ShellCompRequestCmd if !includeDesc { compCmd = ShellCompNoDescRequestCmd @@ -121,7 +121,7 @@ _%[1]s() fi # Prepare the command to obtain completions - requestComp="%[9]s=${%[9]s-%[10]s} ${words[1]} %[2]s ${words[2,-1]}" + requestComp="${words[1]} %[2]s ${words[2,-1]}" if [ "${lastChar}" = "" ]; then # If the last parameter is complete (there is a space following it) # We add an extra empty parameter so we can indicate this to the go completion code. @@ -280,8 +280,8 @@ _%[1]s() if [ "$funcstack[1]" = "_%[1]s" ]; then _%[1]s fi -`, cmd.Name(), compCmd, +`, name, compCmd, ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, - activeHelpMarker, activeHelpEnvVar(cmd.Name()), cmd.ActiveHelpConfig)) + activeHelpMarker)) }