Skip to content

Commit

Permalink
Only allow configuring ActiveHelp through env vars
Browse files Browse the repository at this point in the history
Signed-off-by: Marc Khouzam <marc.khouzam@montreal.ca>
  • Loading branch information
marckhouzam committed Mar 18, 2022
1 parent 3f77aba commit 9dc2b50
Show file tree
Hide file tree
Showing 7 changed files with 56 additions and 116 deletions.
31 changes: 14 additions & 17 deletions active_help.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
// <PROGRAM>_ACTIVE_HELP where <PROGRAM> 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 <PROGRAM>_ACTIVE_HELP where <PROGRAM> 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
}
67 changes: 21 additions & 46 deletions active_help.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand All @@ -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 `<PROGRAM>_ACTIVE_HELP` where `<PROGRAM>` 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" {
Expand All @@ -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 `<PROGRAM>_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 `<PROGRAM>_ACTIVE_HELP` is set to.

**Note 3**: If the user does not set `<PROGRAM>_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 `<PROGRAM>_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 `<PROGRAM>_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<ENTER>
bitnami/haproxy
Expand Down
32 changes: 10 additions & 22 deletions active_help_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
})
Expand Down Expand Up @@ -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
}
Expand Down
10 changes: 5 additions & 5 deletions bash_completionsV2.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -350,10 +350,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.
Expand Down
17 changes: 0 additions & 17 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 wasn't
Expand Down
5 changes: 1 addition & 4 deletions completions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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...)
}
Expand Down
10 changes: 5 additions & 5 deletions zsh_completions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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))
}

0 comments on commit 9dc2b50

Please sign in to comment.