-
Notifications
You must be signed in to change notification settings - Fork 2.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Active Help support #1482
Merged
Merged
Add Active Help support #1482
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
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)) | ||
} | ||
|
||
// 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 _. | ||
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, "-", "_") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
# 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/ | ||
``` | ||
|
||
**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: | ||
- 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 | ||
``` | ||
**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 | ||
|
||
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, 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. | ||
|
||
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 `_`. The variable should be set by the user to whatever | ||
Active Help configuration values are supported by the program. | ||
|
||
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. | ||
|
||
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 := cobra.GetActiveHelpConfig(cmd) | ||
|
||
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 `<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. | ||
|
||
**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 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 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. 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 | ||
``` |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure with the API here, why do I give the compArray as parameter and get the same value with the appended entry back.
IMO It would make more sense to have it like this:
and then in you code call
With this the caller has control over the append. While I do not think we need to worry about performance here I believe it is better when you call append only once.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I struggled to define a good API to add ActiveHelp messages. Here is how I remember choosing the
AppendActiveHelp(compArray []string, activeHelpStr string)
API.ValidArgsFunction
; the two concepts could be seen as independent by users (completions and activeHelp messages). I therefore felt that by requiring the user to specify an array (and saying in the godoc that the array should be the one also containing the completion choices) we would make it much clearer that activeHelp messages should be included in the array of completions.append()
which is why I called itAppendActiveHelp()
and required the user to assign the result to an array (just likeappend()
). All this in the hope to build on existing knowledge of an existing API.If we end up preferring
we should probably rename the method to avoid using the word "append". We could create it as
CreateActiveHelpMsg()
for example.But then I worry again that a user won't know easily what to do after having created an activeHelp message. Will they realize they must include it in the completions array?
Some people had trouble understanding what the arguments to
ValidArgsFunction(cmd, args, toComplete)
represented... So I don't want to assume anything is clear 😄There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmmmm this is a tricky one. I'm not sure on this.
I think I'm a fan of using the API presented here by Marc, but I'm warry of introducing more complexity to how users get their completions into their program.
But from my understanding of this, it's completely opt in? It's not another required step users must take to get their completions working?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct, the existing API for normal completions is not affected at all. ActiveHelp is 100% opt-in and can be completely ignored by programs using Cobra.