Skip to content

Commit

Permalink
Use env var instead of flag for ActiveHelp
Browse files Browse the repository at this point in the history
Use an environment variable containing the program name to set the
ActiveHelp configuration.  This is important because different programs
can use different configurations, so a global variable would not work.

But we also provide a global COBRA_ACTIVE_HELP environment variable
which can only be set to 0 and which will disable Active Help for ALL
programs based on Cobra.

Signed-off-by: Marc Khouzam <marc.khouzam@montreal.ca>
  • Loading branch information
marckhouzam committed Oct 9, 2021
1 parent c47be85 commit 491bdad
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 65 deletions.
45 changes: 36 additions & 9 deletions active_help.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,17 @@ 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 level of configurability of Active Help that it wants to offer. Implementing the configuration of Active Help through Cobra is done as follows:
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.

1. Allow a user to specify the Active Help configuration she wants. This would normally be done when the user requests the generation of the shell completion script
1. Specify the Active Help configuration by setting the `rootCmd.ActiveHelpConfig` string before calling the Cobra API that generates the shell completion script
1. When in `cmd.ValidArgsFunction(...)` or a flag's completion function, read the configuration from the `cmd.ActiveHelpConfig` field and select what Active Help messages should or should not be added
### Configuration using a flag

One way to configure Active Help is 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
then program should specify that configuration by setting the `rootCmd.ActiveHelpConfig` string
before calling the Cobra API that generates the shell completion script. And finally 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 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:
```
Expand Down Expand Up @@ -124,26 +130,47 @@ ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([
return comps, cobra.ShellCompDirectiveNoFileComp
},
```
**Note**: 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 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.**

### Configuration using an environment variable

Another approach for a user 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 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.

**Note**: 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.
For example, say `helm` supports 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.

The program would access this value in the same fashion as explained above,
using `cmd.ActiveHelpConfig`. However when using the environment variable configuration approach,
the program does not need to set `rootCmd.ActiveHelpConfig` before generating the completion
scripts.

## 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.

When debugging with the `__complete` command, if you want to specify different Active Help configurations, you should use the `--__activeHelpCfg` flag (as is done by the generated completion scripts). 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 (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:
```
$ bin/helm __complete --__activeHelpCfg=1 install wordpress bitnami/h<ENTER>
$ HELM_ACTIVE_HELP=1 bin/helm __complete install wordpress bitnami/h<ENTER>
bitnami/haproxy
bitnami/harbor
_activeHelp_ WARNING: cannot re-use a name that is still in use
:0
Completion ended with directive: ShellCompDirectiveDefault
$ bin/helm __complete --__activeHelpCfg=0 install wordpress bitnami/h<ENTER>
$ HELM_ACTIVE_HELP=0 bin/helm __complete install wordpress bitnami/h<ENTER>
bitnami/haproxy
bitnami/harbor
:0
Completion ended with directive: ShellCompDirectiveDefault
```

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
```
4 changes: 2 additions & 2 deletions bash_completions.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ __%[1]s_handle_go_custom_completion()
# Calling ${words[0]} instead of directly %[1]s allows to handle aliases
args=("${words[@]:1}")
# Disable ActiveHelp which is not supported for bash completion v1
requestComp="${words[0]} %[2]s --%[8]s=0 ${args[*]}"
requestComp="%[8]s=0 ${words[0]} %[2]s ${args[*]}"
lastParam=${words[$((${#words[@]}-1))]}
lastChar=${lastParam:$((${#lastParam}-1)):1}
Expand Down Expand Up @@ -378,7 +378,7 @@ __%[1]s_handle_word()
`, name, ShellCompNoDescRequestCmd,
ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp,
ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, activeHelpCfgFlagName))
ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, activeHelpEnvVar(name)))
}

func writePostscript(buf io.StringWriter, name string) {
Expand Down
4 changes: 2 additions & 2 deletions bash_completionsV2.go
Original file line number Diff line number Diff line change
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="${words[0]} %[2]s --%[9]s=%[10]s ${args[*]}"
requestComp="%[9]s=${%[9]s-%[10]s} ${words[0]} %[2]s ${args[*]}"
lastParam=${words[$((${#words[@]}-1))]}
lastChar=${lastParam:$((${#lastParam}-1)):1}
Expand Down Expand Up @@ -316,7 +316,7 @@ fi
`, cmd.Name(), compCmd,
ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp,
ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs,
activeHelpMarker, activeHelpCfgFlagName, cmd.Root().ActiveHelpConfig))
activeHelpMarker, activeHelpEnvVar(cmd.Name()), cmd.Root().ActiveHelpConfig))
}

// GenBashCompletionFileV2 generates Bash completion version 2.
Expand Down
69 changes: 25 additions & 44 deletions completions.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,6 @@ var flagCompletionFunctions = map[*pflag.Flag]func(cmd *Command, args []string,
// lock for reading and writing from flagCompletionFunctions
var flagCompletionMutex = &sync.RWMutex{}

var (
activeHelpCfgFlagName = "__activeHelpCfg"
activeHelpCfgFlagValue string
)

// ShellCompDirective is a bit map representing the different behaviors the shell
// can be instructed to have once completions have been provided.
type ShellCompDirective int
Expand Down Expand Up @@ -87,7 +82,10 @@ const (
compCmdNoDescFlagDesc = "disable completion descriptions"
compCmdNoDescFlagDefault = false

activeHelpMarker = "_activeHelp_ "
activeHelpMarker = "_activeHelp_ "
activeHelpEnvVarSuffix = "_ACTIVE_HELP"
activeHelpGlobalEnvVar = "COBRA_ACTIVE_HELP"
activeHelpGlobalDisable = "0"
)

// CompletionOptions are the options to control shell completion
Expand All @@ -102,6 +100,11 @@ type CompletionOptions struct {
DisableDescriptions bool
}

func activeHelpEnvVar(name string) string {
activeHelpEnvVar := strings.ToUpper(fmt.Sprintf("%s%s", name, activeHelpEnvVarSuffix))
return strings.ReplaceAll(activeHelpEnvVar, "-", "_")
}

// NoFileCompletions can be used to disable file completion for commands that should
// not trigger file completions.
func NoFileCompletions(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
Expand Down Expand Up @@ -166,21 +169,16 @@ func (d ShellCompDirective) string() string {
// Adds a special hidden command that can be used to request custom completions.
func (c *Command) initCompleteCmd(args []string) {
completeCmd := &Command{
Use: fmt.Sprintf("%s [command-line]", ShellCompRequestCmd),
Aliases: []string{ShellCompNoDescRequestCmd},
Hidden: true,
DisableFlagParsing: true,
Args: MinimumNArgs(1),
Short: "Request shell completion choices for the specified command-line",
Use: fmt.Sprintf("%s [command-line]", ShellCompRequestCmd),
Aliases: []string{ShellCompNoDescRequestCmd},
DisableFlagsInUseLine: true,
Hidden: true,
DisableFlagParsing: true,
Args: MinimumNArgs(1),
Short: "Request shell completion choices for the specified command-line",
Long: fmt.Sprintf("%[2]s is a special command that is used by the shell completion logic\n%[1]s",
"to request completion choices for the specified command-line.", ShellCompRequestCmd),
Run: func(cmd *Command, args []string) {
args, _ = cmd.extractCompleteCmdFlags(args)
if err := cmd.ValidateArgs(args); err != nil {
CompErrorln(err.Error())
return
}

finalCmd, completions, directive, err := cmd.getCompletions(args)
if err != nil {
CompErrorln(err.Error())
Expand All @@ -191,7 +189,7 @@ func (c *Command) initCompleteCmd(args []string) {

noDescriptions := (cmd.CalledAs() == ShellCompNoDescRequestCmd)
for _, comp := range completions {
if finalCmd.ActiveHelpConfig == "0" {
if finalCmd.ActiveHelpConfig == activeHelpGlobalDisable {
// Remove all activeHelp entries in this case
if strings.HasPrefix(comp, activeHelpMarker) {
continue
Expand Down Expand Up @@ -241,30 +239,6 @@ func (c *Command) initCompleteCmd(args []string) {
}
}

func (c *Command) extractCompleteCmdFlags(args []string) ([]string, error) {
var remainingArgs []string
validFlags := []string{activeHelpCfgFlagName}
for _, validFlag := range validFlags {
for i := 0; i < len(args); i++ {
arg := args[i]
if strings.HasPrefix(arg, "--"+validFlag) {
if pos := strings.Index(arg, "="); pos > -1 {
activeHelpCfgFlagValue = arg[pos+1:]
} else {
// Value is in the next argument
i++
if i < len(args) {
activeHelpCfgFlagValue = args[i]
}
}
} else {
remainingArgs = append(remainingArgs, arg)
}
}
}
return remainingArgs, nil
}

func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDirective, error) {
// The last argument, which is not completely typed by the user,
// should not be part of the list of arguments
Expand Down Expand Up @@ -474,7 +448,14 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
// First set 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.
finalCmd.ActiveHelpConfig = activeHelpCfgFlagValue
// First check the global environment variable to see if it is
// disabling active help, and if it is not, use the program-specific var.
activeHelpVar := os.Getenv(activeHelpGlobalEnvVar)
if activeHelpVar != activeHelpGlobalDisable {
activeHelpVar = os.Getenv(activeHelpEnvVar(c.Root().Name()))
}

finalCmd.ActiveHelpConfig = activeHelpVar
finalCmd.Root().ActiveHelpConfig = finalCmd.ActiveHelpConfig

comps, directive = completionFn(finalCmd, finalArgs, toComplete)
Expand Down
4 changes: 2 additions & 2 deletions fish_completions.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ function __%[1]s_perform_completion
__%[1]s_debug "last arg: $lastArg"
# Disable ActiveHelp which is not supported for fish shell
set -l requestComp "$args[1] %[3]s --%[9]s=0 $args[2..-1] $lastArg"
set -l requestComp "%[9]s=0 $args[1] %[3]s $args[2..-1] $lastArg"
__%[1]s_debug "Calling $requestComp"
set -l results (eval $requestComp 2> /dev/null)
Expand Down Expand Up @@ -197,7 +197,7 @@ complete -c %[2]s -n '__%[1]s_prepare_completions' -f -a '$__%[1]s_comp_results'
`, nameForVar, name, compCmd,
ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp,
ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, activeHelpCfgFlagName))
ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, activeHelpEnvVar(name)))
}

// GenFishCompletion generates fish completion file and writes to the passed writer.
Expand Down
10 changes: 6 additions & 4 deletions powershell_completions.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,8 @@ Register-ArgumentCompleter -CommandName '%[1]s' -ScriptBlock {
# Prepare the command to request completions for the program.
# Split the command at the first space to separate the program and arguments.
$Program,$Arguments = $Command.Split(" ",2)
# Also disable ActiveHelp which is not supported for Powershell
$RequestComp="$Program %[2]s --%[8]s=0 $Arguments"
$RequestComp="$Program %[2]s $Arguments"
__%[1]s_debug "RequestComp: $RequestComp"
# we cannot use $WordToComplete because it
Expand Down Expand Up @@ -92,6 +91,9 @@ Register-ArgumentCompleter -CommandName '%[1]s' -ScriptBlock {
}
__%[1]s_debug "Calling $RequestComp"
# First disable ActiveHelp which is not supported for Powershell
$env:%[8]s=0
#call the command store the output in $out and redirect stderr and stdout to null
# $Out is an array contains each line per element
Invoke-Expression -OutVariable out "$RequestComp" 2>&1 | Out-Null
Expand Down Expand Up @@ -243,7 +245,7 @@ Register-ArgumentCompleter -CommandName '%[1]s' -ScriptBlock {
}
`, name, compCmd,
ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp,
ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, activeHelpCfgFlagName))
ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, activeHelpEnvVar(name)))
}

func (c *Command) genPowerShellCompletion(w io.Writer, includeDesc bool) error {
Expand Down
4 changes: 2 additions & 2 deletions zsh_completions.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ _%[1]s()
fi
# Prepare the command to obtain completions
requestComp="${words[1]} %[2]s --%[9]s=%[10]s ${words[2,-1]}"
requestComp="%[9]s=${%[9]s-%[10]s} ${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 @@ -283,5 +283,5 @@ fi
`, cmd.Name(), compCmd,
ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp,
ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs,
activeHelpMarker, activeHelpCfgFlagName, cmd.Root().ActiveHelpConfig))
activeHelpMarker, activeHelpEnvVar(cmd.Name()), cmd.ActiveHelpConfig))
}

0 comments on commit 491bdad

Please sign in to comment.