Add support for ActiveHelp

Signed-off-by: Marc Khouzam <marc.khouzam@montreal.ca>
This commit is contained in:
Marc Khouzam 2021-08-06 17:18:02 -04:00 committed by Marc Khouzam
parent ca8e3c2779
commit 25e8b5e735
16 changed files with 844 additions and 23 deletions

52
active_help.go Normal file
View file

@ -0,0 +1,52 @@
package cobra
import (
"fmt"
"os"
"strings"
)
const (
activeHelpMarker = "_activeHelp_ "
// The below values should not be changed: programs will be using them explicitly
// in their user documentation, and users will be using them explicitly.
activeHelpEnvVarSuffix = "_ACTIVE_HELP"
activeHelpGlobalEnvVar = "COBRA_ACTIVE_HELP"
activeHelpGlobalDisable = "0"
)
// AppendActiveHelp adds the specified string to the specified array to be used as ActiveHelp.
// Such strings will be processed by the completion script and will be shown as ActiveHelp
// to the user.
// The array parameter should be the array that will contain the completions.
// This function can be called multiple times before and/or after completions are added to
// the array. Each time this function is called with the same array, the new
// ActiveHelp line will be shown below the previous ones when completion is triggered.
func AppendActiveHelp(compArray []string, activeHelpStr string) []string {
return append(compArray, fmt.Sprintf("%s%s", activeHelpMarker, activeHelpStr))
}
// 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 {
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
}

182
active_help.md Normal file
View file

@ -0,0 +1,182 @@
# Active Help
Active Help is a framework provided by Cobra which allows a program to define messages (hints, warnings, etc) that will be printed during program usage. It aims to make it easier for your users to learn how to use your program. If configured by the program, Active Help is printed when the user triggers shell completion.
For example,
```
bash-5.1$ helm repo add [tab]
You must choose a name for the repo you are adding.
bash-5.1$ bin/helm package [tab]
Please specify the path to the chart to package
bash-5.1$ bin/helm package [tab][tab]
bin/ internal/ scripts/ pkg/ testdata/
```
## Supported shells
Active Help is currently only supported for the following shells:
- Bash (using [bash completion V2](shell_completions.md#bash-completion-v2) only). Note that bash 4.4 or higher is required for the prompt to appear when an Active Help message is printed.
- Zsh
## Adding Active Help messages
As Active Help uses the shell completion system, the implementation of Active Help messages is done by enhancing custom dynamic completions. If you are not familiar with dynamic completions, please refer to [Shell Completions](shell_completions.md).
Adding Active Help is done through the use of the `cobra.AppendActiveHelp(...)` function, where the program repeatedly adds Active Help messages to the list of completions. Keep reading for details.
### Active Help for nouns
Adding Active Help when completing a noun is done within the `ValidArgsFunction(...)` of a command. Please notice the use of `cobra.AppendActiveHelp(...)` in the following example:
```go
cmd := &cobra.Command{
Use: "add [NAME] [URL]",
Short: "add a chart repository",
Args: require.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
return addRepo(args)
},
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
var comps []string
if len(args) == 0 {
comps = cobra.AppendActiveHelp(comps, "You must choose a name for the repo you are adding")
} else if len(args) == 1 {
comps = cobra.AppendActiveHelp(comps, "You must specify the URL for the repo you are adding")
} else {
comps = cobra.AppendActiveHelp(comps, "This command does not take any more arguments")
}
return comps, cobra.ShellCompDirectiveNoFileComp
},
}
```
The example above defines the completions (none, in this specific example) as well as the Active Help messages for the `helm repo add` command. It yields the following behavior:
```
bash-5.1$ helm repo add [tab]
You must choose a name for the repo you are adding
bash-5.1$ helm repo add grafana [tab]
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
```
### Active Help for flags
Providing Active Help for flags is done in the same fashion as for nouns, but using the completion function registered for the flag. For example:
```go
_ = cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 2 {
return cobra.AppendActiveHelp(nil, "You must first specify the chart to install before the --version flag can be completed"), cobra.ShellCompDirectiveNoFileComp
}
return compVersionFlag(args[1], toComplete)
})
```
The example above prints an Active Help message when not enough information was given by the user to complete the `--version` flag.
```
bash-5.1$ bin/helm install myrelease --version 2.0.[tab]
You must first specify the chart to install before the --version flag can be completed
bash-5.1$ bin/helm install myrelease bitnami/solr --version 2.0.[tab][tab]
2.0.1 2.0.2 2.0.3
```
## 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
One 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.
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.
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:
```go
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
activeHelpLevel := cmd.ActiveHelpConfig
var comps []string
if len(args) == 0 {
if activeHelpLevel != "off" {
comps = cobra.AppendActiveHelp(comps, "You must choose a name for the repo you are adding")
}
} else if len(args) == 1 {
if activeHelpLevel != "off" {
comps = cobra.AppendActiveHelp(comps, "You must specify the URL for the repo you are adding")
}
} else {
if activeHelpLevel == "local" {
comps = cobra.AppendActiveHelp(comps, "This command does not take any more arguments")
}
}
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)
```
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.
## 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
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.
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:
```
$ 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
$ HELM_ACTIVE_HELP=0 bin/helm __complete install wordpress bitnami/h<ENTER>
bitnami/haproxy
bitnami/harbor
:0
Completion ended with directive: ShellCompDirectiveDefault
```

398
active_help_test.go Normal file
View file

@ -0,0 +1,398 @@
package cobra
import (
"fmt"
"os"
"strings"
"testing"
)
const (
activeHelpMessage = "This is an activeHelp message"
activeHelpMessage2 = "This is the rest of the activeHelp message"
)
func TestActiveHelpAlone(t *testing.T) {
rootCmd := &Command{
Use: "root",
Run: emptyRun,
}
activeHelpFunc := func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
comps := AppendActiveHelp(nil, activeHelpMessage)
return comps, ShellCompDirectiveDefault
}
// Test that activeHelp can be added to a root command
rootCmd.ValidArgsFunction = activeHelpFunc
output, err := executeCommand(rootCmd, ShellCompNoDescRequestCmd, "")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
expected := strings.Join([]string{
fmt.Sprintf("%s%s", activeHelpMarker, activeHelpMessage),
":0",
"Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n")
if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}
rootCmd.ValidArgsFunction = nil
// Test that activeHelp can be added to a child command
childCmd := &Command{
Use: "thechild",
Short: "The child command",
Run: emptyRun,
}
rootCmd.AddCommand(childCmd)
childCmd.ValidArgsFunction = activeHelpFunc
output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "thechild", "")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
expected = strings.Join([]string{
fmt.Sprintf("%s%s", activeHelpMarker, activeHelpMessage),
":0",
"Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n")
if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}
}
func TestActiveHelpWithComps(t *testing.T) {
rootCmd := &Command{
Use: "root",
Run: emptyRun,
}
childCmd := &Command{
Use: "thechild",
Short: "The child command",
Run: emptyRun,
}
rootCmd.AddCommand(childCmd)
// Test that activeHelp can be added following other completions
childCmd.ValidArgsFunction = func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
comps := []string{"first", "second"}
comps = AppendActiveHelp(comps, activeHelpMessage)
return comps, ShellCompDirectiveDefault
}
output, err := executeCommand(rootCmd, ShellCompNoDescRequestCmd, "thechild", "")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
expected := strings.Join([]string{
"first",
"second",
fmt.Sprintf("%s%s", activeHelpMarker, activeHelpMessage),
":0",
"Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n")
if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}
// Test that activeHelp can be added preceding other completions
childCmd.ValidArgsFunction = func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
var comps []string
comps = AppendActiveHelp(comps, activeHelpMessage)
comps = append(comps, []string{"first", "second"}...)
return comps, ShellCompDirectiveDefault
}
output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "thechild", "")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
expected = strings.Join([]string{
fmt.Sprintf("%s%s", activeHelpMarker, activeHelpMessage),
"first",
"second",
":0",
"Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n")
if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}
// Test that activeHelp can be added interleaved with other completions
childCmd.ValidArgsFunction = func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
comps := []string{"first"}
comps = AppendActiveHelp(comps, activeHelpMessage)
comps = append(comps, "second")
return comps, ShellCompDirectiveDefault
}
output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "thechild", "")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
expected = strings.Join([]string{
"first",
fmt.Sprintf("%s%s", activeHelpMarker, activeHelpMessage),
"second",
":0",
"Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n")
if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}
}
func TestMultiActiveHelp(t *testing.T) {
rootCmd := &Command{
Use: "root",
Run: emptyRun,
}
childCmd := &Command{
Use: "thechild",
Short: "The child command",
Run: emptyRun,
}
rootCmd.AddCommand(childCmd)
// Test that multiple activeHelp message can be added
childCmd.ValidArgsFunction = func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
comps := AppendActiveHelp(nil, activeHelpMessage)
comps = AppendActiveHelp(comps, activeHelpMessage2)
return comps, ShellCompDirectiveNoFileComp
}
output, err := executeCommand(rootCmd, ShellCompNoDescRequestCmd, "thechild", "")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
expected := strings.Join([]string{
fmt.Sprintf("%s%s", activeHelpMarker, activeHelpMessage),
fmt.Sprintf("%s%s", activeHelpMarker, activeHelpMessage2),
":4",
"Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n")
if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}
// Test that multiple activeHelp messages can be used along with completions
childCmd.ValidArgsFunction = func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
comps := []string{"first"}
comps = AppendActiveHelp(comps, activeHelpMessage)
comps = append(comps, "second")
comps = AppendActiveHelp(comps, activeHelpMessage2)
return comps, ShellCompDirectiveNoFileComp
}
output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "thechild", "")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
expected = strings.Join([]string{
"first",
fmt.Sprintf("%s%s", activeHelpMarker, activeHelpMessage),
"second",
fmt.Sprintf("%s%s", activeHelpMarker, activeHelpMessage2),
":4",
"Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n")
if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}
}
func TestActiveHelpForFlag(t *testing.T) {
rootCmd := &Command{
Use: "root",
Run: emptyRun,
}
flagname := "flag"
rootCmd.Flags().String(flagname, "", "A flag")
// Test that multiple activeHelp message can be added
_ = rootCmd.RegisterFlagCompletionFunc(flagname, func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
comps := []string{"first"}
comps = AppendActiveHelp(comps, activeHelpMessage)
comps = append(comps, "second")
comps = AppendActiveHelp(comps, activeHelpMessage2)
return comps, ShellCompDirectiveNoFileComp
})
output, err := executeCommand(rootCmd, ShellCompNoDescRequestCmd, "--flag", "")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
expected := strings.Join([]string{
"first",
fmt.Sprintf("%s%s", activeHelpMarker, activeHelpMessage),
"second",
fmt.Sprintf("%s%s", activeHelpMarker, activeHelpMessage2),
":4",
"Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n")
if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}
}
func TestConfigActiveHelp(t *testing.T) {
rootCmd := &Command{
Use: "root",
Run: emptyRun,
}
childCmd := &Command{
Use: "thechild",
Short: "The child command",
Run: emptyRun,
}
rootCmd.AddCommand(childCmd)
activeHelpCfg := "someconfig,anotherconfig"
// Set the variable that the completions scripts will 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)
}
return nil, ShellCompDirectiveDefault
}
_, err := executeCommand(rootCmd, ShellCompNoDescRequestCmd, "thechild", "")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
// Test active help config for a flag
activeHelpCfg = "a config for a flag"
// Set the variable that the completions scripts will be setting
os.Setenv(activeHelpEnvVar(rootCmd.Name()), activeHelpCfg)
flagname := "flag"
childCmd.Flags().String(flagname, "", "A flag")
// 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)
}
return nil, ShellCompDirectiveDefault
})
_, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "thechild", "--flag", "")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
}
func TestDisableActiveHelp(t *testing.T) {
rootCmd := &Command{
Use: "root",
Run: emptyRun,
}
childCmd := &Command{
Use: "thechild",
Short: "The child command",
Run: emptyRun,
}
rootCmd.AddCommand(childCmd)
// Test the disabling of activeHelp using the specific program
// environment variable that the completions scripts will be setting.
// Make sure the disabling value is "0" by hard-coding it in the tests;
// this is for backwards-compatibility as programs will be using this value.
os.Setenv(activeHelpEnvVar(rootCmd.Name()), "0")
childCmd.ValidArgsFunction = func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
comps := []string{"first"}
comps = AppendActiveHelp(comps, activeHelpMessage)
return comps, ShellCompDirectiveDefault
}
output, err := executeCommand(rootCmd, ShellCompNoDescRequestCmd, "thechild", "")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
os.Unsetenv(activeHelpEnvVar(rootCmd.Name()))
// Make sure there is no ActiveHelp in the output
expected := strings.Join([]string{
"first",
":0",
"Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n")
if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}
// Now test the global disabling of ActiveHelp
os.Setenv(activeHelpGlobalEnvVar, "0")
// Set the specific variable, to make sure it is ignored when the global env
// var is set properly
os.Setenv(activeHelpEnvVar(rootCmd.Name()), "1")
output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "thechild", "")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
// Make sure there is no ActiveHelp in the output
expected = strings.Join([]string{
"first",
":0",
"Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n")
if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}
// Make sure that if the global env variable is set to anything else than
// the disable value it is ignored
os.Setenv(activeHelpGlobalEnvVar, "on")
// Set the specific variable, to make sure it is used (while ignoring the global env var)
activeHelpCfg := "1"
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)
}
return nil, ShellCompDirectiveDefault
}
_, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "thechild", "")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
}

View file

@ -73,7 +73,8 @@ __%[1]s_handle_go_custom_completion()
# Prepare the command to request completions for the program. # Prepare the command to request completions for the program.
# Calling ${words[0]} instead of directly %[1]s allows to handle aliases # Calling ${words[0]} instead of directly %[1]s allows to handle aliases
args=("${words[@]:1}") args=("${words[@]:1}")
requestComp="${words[0]} %[2]s ${args[*]}" # Disable ActiveHelp which is not supported for bash completion v1
requestComp="%[8]s=0 ${words[0]} %[2]s ${args[*]}"
lastParam=${words[$((${#words[@]}-1))]} lastParam=${words[$((${#words[@]}-1))]}
lastChar=${lastParam:$((${#lastParam}-1)):1} lastChar=${lastParam:$((${#lastParam}-1)):1}
@ -383,7 +384,7 @@ __%[1]s_handle_word()
`, name, ShellCompNoDescRequestCmd, `, name, ShellCompNoDescRequestCmd,
ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp,
ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs)) ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, activeHelpEnvVar(name)))
} }
func writePostscript(buf io.StringWriter, name string) { func writePostscript(buf io.StringWriter, name string) {

View file

@ -9,12 +9,12 @@ import (
func (c *Command) genBashCompletion(w io.Writer, includeDesc bool) error { func (c *Command) genBashCompletion(w io.Writer, includeDesc bool) error {
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
genBashComp(buf, c.Name(), includeDesc) genBashComp(buf, c, includeDesc)
_, err := buf.WriteTo(w) _, err := buf.WriteTo(w)
return err return err
} }
func genBashComp(buf io.StringWriter, name string, includeDesc bool) { func genBashComp(buf io.StringWriter, cmd *Command, includeDesc bool) {
compCmd := ShellCompRequestCmd compCmd := ShellCompRequestCmd
if !includeDesc { if !includeDesc {
compCmd = ShellCompNoDescRequestCmd compCmd = ShellCompNoDescRequestCmd
@ -45,7 +45,7 @@ __%[1]s_get_completion_results() {
# Prepare the command to request completions for the program. # Prepare the command to request completions for the program.
# Calling ${words[0]} instead of directly %[1]s allows to handle aliases # Calling ${words[0]} instead of directly %[1]s allows to handle aliases
args=("${words[@]:1}") args=("${words[@]:1}")
requestComp="${words[0]} %[2]s ${args[*]}" requestComp="%[9]s=${%[9]s-%[10]s} ${words[0]} %[2]s ${args[*]}"
lastParam=${words[$((${#words[@]}-1))]} lastParam=${words[$((${#words[@]}-1))]}
lastChar=${lastParam:$((${#lastParam}-1)):1} lastChar=${lastParam:$((${#lastParam}-1)):1}
@ -111,13 +111,18 @@ __%[1]s_process_completion_results() {
fi fi
fi fi
# Separate activeHelp from normal completions
local completions=()
local activeHelp=()
__%[1]s_extract_activeHelp
if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then
# File extension filtering # File extension filtering
local fullFilter filter filteringCmd local fullFilter filter filteringCmd
# Do not use quotes around the $out variable or else newline # Do not use quotes around the $completions variable or else newline
# characters will be kept. # characters will be kept.
for filter in ${out}; do for filter in ${completions[*]}; do
fullFilter+="$filter|" fullFilter+="$filter|"
done done
@ -129,7 +134,7 @@ __%[1]s_process_completion_results() {
# Use printf to strip any trailing newline # Use printf to strip any trailing newline
local subdir local subdir
subdir=$(printf "%%s" "${out}") subdir=$(printf "%%s" "${completions[0]}")
if [ -n "$subdir" ]; then if [ -n "$subdir" ]; then
__%[1]s_debug "Listing directories in $subdir" __%[1]s_debug "Listing directories in $subdir"
pushd "$subdir" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return pushd "$subdir" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return
@ -143,6 +148,43 @@ __%[1]s_process_completion_results() {
__%[1]s_handle_special_char "$cur" : __%[1]s_handle_special_char "$cur" :
__%[1]s_handle_special_char "$cur" = __%[1]s_handle_special_char "$cur" =
# Print the activeHelp statements before we finish
if [ ${#activeHelp} -ne 0 ]; then
printf "\n";
printf "%%s\n" "${activeHelp[@]}"
printf "\n"
# The prompt format is only available from bash 4.4.
# We test if it is available before using it.
if (x=${PS1@P}) 2> /dev/null; then
printf "%%s" "${PS1@P}${COMP_LINE[@]}"
else
# Can't print the prompt. Just print the
# text the user had typed, it is workable enough.
printf "%%s" "${COMP_LINE[@]}"
fi
fi
}
# Separate activeHelp lines from real completions.
# Fills the $activeHelp and $completions arrays.
__%[1]s_extract_activeHelp() {
local activeHelpMarker="%[8]s"
local endIndex=${#activeHelpMarker}
while IFS='' read -r comp; do
if [ "${comp:0:endIndex}" = "$activeHelpMarker" ]; then
comp=${comp:endIndex}
__%[1]s_debug "ActiveHelp found: $comp"
if [ -n "$comp" ]; then
activeHelp+=("$comp")
fi
else
# Not an activeHelp line but a normal completion
completions+=("$comp")
fi
done < <(printf "%%s\n" "${out}")
} }
__%[1]s_handle_completion_types() { __%[1]s_handle_completion_types() {
@ -163,7 +205,7 @@ __%[1]s_handle_completion_types() {
if [[ $comp == "$cur"* ]]; then if [[ $comp == "$cur"* ]]; then
COMPREPLY+=("$comp") COMPREPLY+=("$comp")
fi fi
done < <(printf "%%s\n" "${out}") done < <(printf "%%s\n" "${completions[@]}")
;; ;;
*) *)
@ -177,8 +219,8 @@ __%[1]s_handle_standard_completion_case() {
local tab=$'\t' comp local tab=$'\t' comp
# Short circuit to optimize if we don't have descriptions # Short circuit to optimize if we don't have descriptions
if [[ $out != *$tab* ]]; then if [[ ${completions[*]} != *$tab* ]]; then
IFS=$'\n' read -ra COMPREPLY -d '' < <(IFS=$'\n' compgen -W "$out" -- "$cur") IFS=$'\n' read -ra COMPREPLY -d '' < <(compgen -W "${completions[*]}" -- "$cur")
return 0 return 0
fi fi
@ -195,7 +237,7 @@ __%[1]s_handle_standard_completion_case() {
if ((${#comp}>longest)); then if ((${#comp}>longest)); then
longest=${#comp} longest=${#comp}
fi fi
done < <(printf "%%s\n" "${out}") done < <(printf "%%s\n" "${completions[@]}")
# If there is a single completion left, remove the description text # If there is a single completion left, remove the description text
if [ ${#COMPREPLY[*]} -eq 1 ]; then if [ ${#COMPREPLY[*]} -eq 1 ]; then
@ -303,9 +345,10 @@ else
fi fi
# ex: ts=4 sw=4 et filetype=sh # ex: ts=4 sw=4 et filetype=sh
`, name, compCmd, `, cmd.Name(), compCmd,
ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp,
ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs)) ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs,
activeHelpMarker, activeHelpEnvVar(cmd.Name()), cmd.Root().ActiveHelpConfig))
} }
// GenBashCompletionFileV2 generates Bash completion version 2. // GenBashCompletionFileV2 generates Bash completion version 2.

View file

@ -0,0 +1,19 @@
package cobra
import (
"bytes"
"fmt"
"testing"
)
func TestBashCompletionV2WithActiveHelp(t *testing.T) {
c := &Command{Use: "c", Run: emptyRun}
buf := new(bytes.Buffer)
assertNoErr(t, c.GenBashCompletionV2(buf, true))
output := buf.String()
// check that active help is not being disabled
activeHelpVar := activeHelpEnvVar(c.Name())
checkOmit(t, output, fmt.Sprintf("%s=0", activeHelpVar))
}

View file

@ -261,3 +261,15 @@ func TestBashCompletionTraverseChildren(t *testing.T) {
checkOmit(t, output, `local_nonpersistent_flags+=("--bool-flag")`) checkOmit(t, output, `local_nonpersistent_flags+=("--bool-flag")`)
checkOmit(t, output, `local_nonpersistent_flags+=("-b")`) checkOmit(t, output, `local_nonpersistent_flags+=("-b")`)
} }
func TestBashCompletionNoActiveHelp(t *testing.T) {
c := &Command{Use: "c", Run: emptyRun}
buf := new(bytes.Buffer)
assertNoErr(t, c.GenBashCompletion(buf))
output := buf.String()
// check that active help is being disabled
activeHelpVar := activeHelpEnvVar(c.Name())
check(t, output, fmt.Sprintf("%s=0", activeHelpVar))
}

View file

@ -222,6 +222,23 @@ type Command struct {
// SuggestionsMinimumDistance defines minimum levenshtein distance to display suggestions. // SuggestionsMinimumDistance defines minimum levenshtein distance to display suggestions.
// Must be > 0. // Must be > 0.
SuggestionsMinimumDistance int 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 // Context returns underlying command context. If command was executed

View file

@ -178,6 +178,12 @@ func (c *Command) initCompleteCmd(args []string) {
noDescriptions := (cmd.CalledAs() == ShellCompNoDescRequestCmd) noDescriptions := (cmd.CalledAs() == ShellCompNoDescRequestCmd)
for _, comp := range completions { for _, comp := range completions {
if finalCmd.ActiveHelpConfig == activeHelpGlobalDisable {
// Remove all activeHelp entries in this case
if strings.HasPrefix(comp, activeHelpMarker) {
continue
}
}
if noDescriptions { if noDescriptions {
// Remove any description that may be included following a tab character. // Remove any description that may be included following a tab character.
comp = strings.Split(comp, "\t")[0] comp = strings.Split(comp, "\t")[0]
@ -447,6 +453,9 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
// Go custom completion defined for this flag or command. // Go custom completion defined for this flag or command.
// Call the registered completion function to get the completions. // Call the registered completion function to get the completions.
var comps []string var comps []string
setActiveHelpConfig(finalCmd)
comps, directive = completionFn(finalCmd, finalArgs, toComplete) comps, directive = completionFn(finalCmd, finalArgs, toComplete)
completions = append(completions, comps...) completions = append(completions, comps...)
} }

View file

@ -38,7 +38,8 @@ function __%[1]s_perform_completion
__%[1]s_debug "args: $args" __%[1]s_debug "args: $args"
__%[1]s_debug "last arg: $lastArg" __%[1]s_debug "last arg: $lastArg"
set -l requestComp "$args[1] %[3]s $args[2..-1] $lastArg" # Disable ActiveHelp which is not supported for fish shell
set -l requestComp "%[9]s=0 $args[1] %[3]s $args[2..-1] $lastArg"
__%[1]s_debug "Calling $requestComp" __%[1]s_debug "Calling $requestComp"
set -l results (eval $requestComp 2> /dev/null) set -l results (eval $requestComp 2> /dev/null)
@ -196,7 +197,7 @@ complete -c %[2]s -n '__%[1]s_prepare_completions' -f -a '$__%[1]s_comp_results'
`, nameForVar, name, compCmd, `, nameForVar, name, compCmd,
ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp,
ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs)) ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, activeHelpEnvVar(name)))
} }
// GenFishCompletion generates fish completion file and writes to the passed writer. // GenFishCompletion generates fish completion file and writes to the passed writer.

View file

@ -2,6 +2,7 @@ package cobra
import ( import (
"bytes" "bytes"
"fmt"
"testing" "testing"
) )
@ -67,3 +68,15 @@ func TestProgWithColon(t *testing.T) {
check(t, output, "-c root:colon") check(t, output, "-c root:colon")
checkOmit(t, output, "-c root_colon") checkOmit(t, output, "-c root_colon")
} }
func TestFishCompletionNoActiveHelp(t *testing.T) {
c := &Command{Use: "c", Run: emptyRun}
buf := new(bytes.Buffer)
assertNoErr(t, c.GenFishCompletion(buf, true))
output := buf.String()
// check that active help is being disabled
activeHelpVar := activeHelpEnvVar(c.Name())
check(t, output, fmt.Sprintf("%s=0", activeHelpVar))
}

19
power_completions_test.go Normal file
View file

@ -0,0 +1,19 @@
package cobra
import (
"bytes"
"fmt"
"testing"
)
func TestPwshCompletionNoActiveHelp(t *testing.T) {
c := &Command{Use: "c", Run: emptyRun}
buf := new(bytes.Buffer)
assertNoErr(t, c.GenPowerShellCompletion(buf))
output := buf.String()
// check that active help is being disabled
activeHelpVar := activeHelpEnvVar(c.Name())
check(t, output, fmt.Sprintf("%s=0", activeHelpVar))
}

View file

@ -61,6 +61,7 @@ Register-ArgumentCompleter -CommandName '%[1]s' -ScriptBlock {
# Prepare the command to request completions for the program. # Prepare the command to request completions for the program.
# Split the command at the first space to separate the program and arguments. # Split the command at the first space to separate the program and arguments.
$Program,$Arguments = $Command.Split(" ",2) $Program,$Arguments = $Command.Split(" ",2)
$RequestComp="$Program %[2]s $Arguments" $RequestComp="$Program %[2]s $Arguments"
__%[1]s_debug "RequestComp: $RequestComp" __%[1]s_debug "RequestComp: $RequestComp"
@ -90,11 +91,13 @@ Register-ArgumentCompleter -CommandName '%[1]s' -ScriptBlock {
} }
__%[1]s_debug "Calling $RequestComp" __%[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 #call the command store the output in $out and redirect stderr and stdout to null
# $Out is an array contains each line per element # $Out is an array contains each line per element
Invoke-Expression -OutVariable out "$RequestComp" 2>&1 | Out-Null Invoke-Expression -OutVariable out "$RequestComp" 2>&1 | Out-Null
# get directive from last line # get directive from last line
[int]$Directive = $Out[-1].TrimStart(':') [int]$Directive = $Out[-1].TrimStart(':')
if ($Directive -eq "") { if ($Directive -eq "") {
@ -242,7 +245,7 @@ Register-ArgumentCompleter -CommandName '%[1]s' -ScriptBlock {
} }
`, name, compCmd, `, name, compCmd,
ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp,
ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs)) ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, activeHelpEnvVar(name)))
} }
func (c *Command) genPowerShellCompletion(w io.Writer, includeDesc bool) error { func (c *Command) genPowerShellCompletion(w io.Writer, includeDesc bool) error {

View file

@ -660,3 +660,7 @@ Cobra can generate documentation based on subcommands, flags, etc. Read more abo
## Generating shell completions ## Generating shell completions
Cobra can generate a shell-completion file for the following shells: bash, zsh, fish, PowerShell. If you add more information to your commands, these completions can be amazingly powerful and flexible. Read more about it in [Shell Completions](shell_completions.md). Cobra can generate a shell-completion file for the following shells: bash, zsh, fish, PowerShell. If you add more information to your commands, these completions can be amazingly powerful and flexible. Read more about it in [Shell Completions](shell_completions.md).
## Providing Active Help
Cobra makes use of the shell-completion system to define a framework allowing you to provide Active Help to your users. Active Help are messages (hints, warnings, etc) printed as the program is being used. Read more about it in [Active Help](active_help.md).

View file

@ -65,12 +65,12 @@ func (c *Command) genZshCompletionFile(filename string, includeDesc bool) error
func (c *Command) genZshCompletion(w io.Writer, includeDesc bool) error { func (c *Command) genZshCompletion(w io.Writer, includeDesc bool) error {
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
genZshComp(buf, c.Name(), includeDesc) genZshComp(buf, c, includeDesc)
_, err := buf.WriteTo(w) _, err := buf.WriteTo(w)
return err return err
} }
func genZshComp(buf io.StringWriter, name string, includeDesc bool) { func genZshComp(buf io.StringWriter, cmd *Command, includeDesc bool) {
compCmd := ShellCompRequestCmd compCmd := ShellCompRequestCmd
if !includeDesc { if !includeDesc {
compCmd = ShellCompNoDescRequestCmd compCmd = ShellCompNoDescRequestCmd
@ -121,7 +121,7 @@ _%[1]s()
fi fi
# Prepare the command to obtain completions # Prepare the command to obtain completions
requestComp="${words[1]} %[2]s ${words[2,-1]}" requestComp="%[9]s=${%[9]s-%[10]s} ${words[1]} %[2]s ${words[2,-1]}"
if [ "${lastChar}" = "" ]; then if [ "${lastChar}" = "" ]; then
# If the last parameter is complete (there is a space following it) # 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. # We add an extra empty parameter so we can indicate this to the go completion code.
@ -163,7 +163,24 @@ _%[1]s()
return return
fi fi
local activeHelpMarker="%[8]s"
local endIndex=${#activeHelpMarker}
local startIndex=$((${#activeHelpMarker}+1))
local hasActiveHelp=0
while IFS='\n' read -r comp; do while IFS='\n' read -r comp; do
# Check if this is an activeHelp statement (i.e., prefixed with $activeHelpMarker)
if [ "${comp[1,$endIndex]}" = "$activeHelpMarker" ];then
__%[1]s_debug "ActiveHelp found: $comp"
comp="${comp[$startIndex,-1]}"
if [ -n "$comp" ]; then
compadd -x "${comp}"
__%[1]s_debug "ActiveHelp will need delimiter"
hasActiveHelp=1
fi
continue
fi
if [ -n "$comp" ]; then if [ -n "$comp" ]; then
# If requested, completions are returned with a description. # If requested, completions are returned with a description.
# The description is preceded by a TAB character. # The description is preceded by a TAB character.
@ -180,6 +197,17 @@ _%[1]s()
fi fi
done < <(printf "%%s\n" "${out[@]}") done < <(printf "%%s\n" "${out[@]}")
# Add a delimiter after the activeHelp statements, but only if:
# - there are completions following the activeHelp statements, or
# - file completion will be performed (so there will be choices after the activeHelp)
if [ $hasActiveHelp -eq 1 ]; then
if [ ${#completions} -ne 0 ] || [ $((directive & shellCompDirectiveNoFileComp)) -eq 0 ]; then
__%[1]s_debug "Adding activeHelp delimiter"
compadd -x "--"
hasActiveHelp=0
fi
fi
if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then
__%[1]s_debug "Activating nospace." __%[1]s_debug "Activating nospace."
noSpace="-S ''" noSpace="-S ''"
@ -252,7 +280,8 @@ _%[1]s()
if [ "$funcstack[1]" = "_%[1]s" ]; then if [ "$funcstack[1]" = "_%[1]s" ]; then
_%[1]s _%[1]s
fi fi
`, name, compCmd, `, cmd.Name(), compCmd,
ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp,
ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs)) ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs,
activeHelpMarker, activeHelpEnvVar(cmd.Name()), cmd.ActiveHelpConfig))
} }

19
zsh_completions_test.go Normal file
View file

@ -0,0 +1,19 @@
package cobra
import (
"bytes"
"fmt"
"testing"
)
func TestZshCompletionWithActiveHelp(t *testing.T) {
c := &Command{Use: "c", Run: emptyRun}
buf := new(bytes.Buffer)
assertNoErr(t, c.GenZshCompletion(buf))
output := buf.String()
// check that active help is not being disabled
activeHelpVar := activeHelpEnvVar(c.Name())
checkOmit(t, output, fmt.Sprintf("%s=0", activeHelpVar))
}