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 5e226f0
Show file tree
Hide file tree
Showing 7 changed files with 49 additions and 61 deletions.
15 changes: 10 additions & 5 deletions active_help.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,26 +124,31 @@ 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**: 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.
**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.

## 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 5e226f0

Please sign in to comment.