Add support for custom completions in Go (#1035)

This commit allows programs using Cobra to code their custom completions
in Go instead of Bash.

The new ValidArgsFunction field is added for commands, similarly to
ValidArgs.  For flags, the new function
Command.RegisterFlagCompletionFunc() is added.

When either of the above functions is used, the bash completion script
will call the new hidden command '__complete', passing it all
command-line arguments. The '__complete' command will call
the function specified by Command.ValidArgsFunction or by
Command.RegisterFlagCompletionFunc to obtain completions from the
program itself.

Signed-off-by: Marc Khouzam <marc.khouzam@montreal.ca>
This commit is contained in:
Marc Khouzam 2020-04-03 15:43:43 -04:00 committed by GitHub
parent 138b98f39d
commit b80aeb17fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 955 additions and 57 deletions

View file

@ -58,6 +58,67 @@ __%[1]s_contains_word()
return 1
}
__%[1]s_handle_go_custom_completion()
{
__%[1]s_debug "${FUNCNAME[0]}: cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}"
local out requestComp lastParam lastChar comp directive args
# 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 ${args[*]}"
lastParam=${words[$((${#words[@]}-1))]}
lastChar=${lastParam:$((${#lastParam}-1)):1}
__%[1]s_debug "${FUNCNAME[0]}: lastParam ${lastParam}, lastChar ${lastChar}"
if [ -z "${cur}" ] && [ "${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 method.
__%[1]s_debug "${FUNCNAME[0]}: Adding extra empty parameter"
requestComp="${requestComp} \"\""
fi
__%[1]s_debug "${FUNCNAME[0]}: calling ${requestComp}"
# Use eval to handle any environment variables and such
out=$(eval "${requestComp}" 2>/dev/null)
# Extract the directive integer at the very end of the output following a colon (:)
directive=${out##*:}
# Remove the directive
out=${out%%:*}
if [ "${directive}" = "${out}" ]; then
# There is not directive specified
directive=0
fi
__%[1]s_debug "${FUNCNAME[0]}: the completion directive is: ${directive}"
__%[1]s_debug "${FUNCNAME[0]}: the completions are: ${out[*]}"
if [ $((directive & %[3]d)) -ne 0 ]; then
# Error code. No completion.
__%[1]s_debug "${FUNCNAME[0]}: received error from custom completion go code"
return
else
if [ $((directive & %[4]d)) -ne 0 ]; then
if [[ $(type -t compopt) = "builtin" ]]; then
__%[1]s_debug "${FUNCNAME[0]}: activating no space"
compopt -o nospace
fi
fi
if [ $((directive & %[5]d)) -ne 0 ]; then
if [[ $(type -t compopt) = "builtin" ]]; then
__%[1]s_debug "${FUNCNAME[0]}: activating no file completion"
compopt +o default
fi
fi
while IFS='' read -r comp; do
COMPREPLY+=("$comp")
done < <(compgen -W "${out[*]}" -- "$cur")
fi
}
__%[1]s_handle_reply()
{
__%[1]s_debug "${FUNCNAME[0]}"
@ -121,6 +182,10 @@ __%[1]s_handle_reply()
completions=("${commands[@]}")
if [[ ${#must_have_one_noun[@]} -ne 0 ]]; then
completions=("${must_have_one_noun[@]}")
elif [[ -n "${has_completion_function}" ]]; then
# if a go completion function is provided, defer to that function
completions=()
__%[1]s_handle_go_custom_completion
fi
if [[ ${#must_have_one_flag[@]} -ne 0 ]]; then
completions+=("${must_have_one_flag[@]}")
@ -279,7 +344,7 @@ __%[1]s_handle_word()
__%[1]s_handle_word
}
`, name))
`, name, CompRequestCmd, BashCompDirectiveError, BashCompDirectiveNoSpace, BashCompDirectiveNoFileComp))
}
func writePostscript(buf *bytes.Buffer, name string) {
@ -304,6 +369,7 @@ func writePostscript(buf *bytes.Buffer, name string) {
local commands=("%[1]s")
local must_have_one_flag=()
local must_have_one_noun=()
local has_completion_function
local last_command
local nouns=()
@ -404,7 +470,22 @@ func writeLocalNonPersistentFlag(buf *bytes.Buffer, flag *pflag.Flag) {
buf.WriteString(fmt.Sprintf(format, name))
}
// Setup annotations for go completions for registered flags
func prepareCustomAnnotationsForFlags(cmd *Command) {
for flag := range flagCompletionFunctions {
// Make sure the completion script calls the __*_go_custom_completion function for
// every registered flag. We need to do this here (and not when the flag was registered
// for completion) so that we can know the root command name for the prefix
// of __<prefix>_go_custom_completion
if flag.Annotations == nil {
flag.Annotations = map[string][]string{}
}
flag.Annotations[BashCompCustom] = []string{fmt.Sprintf("__%[1]s_handle_go_custom_completion", cmd.Root().Name())}
}
}
func writeFlags(buf *bytes.Buffer, cmd *Command) {
prepareCustomAnnotationsForFlags(cmd)
buf.WriteString(` flags=()
two_word_flags=()
local_nonpersistent_flags=()
@ -469,6 +550,9 @@ func writeRequiredNouns(buf *bytes.Buffer, cmd *Command) {
for _, value := range cmd.ValidArgs {
buf.WriteString(fmt.Sprintf(" must_have_one_noun+=(%q)\n", value))
}
if cmd.ValidArgsFunction != nil {
buf.WriteString(" has_completion_function=1\n")
}
}
func writeCmdAliases(buf *bytes.Buffer, cmd *Command) {

View file

@ -56,7 +56,147 @@ func main() {
`out.sh` will get you completions of subcommands and flags. Copy it to `/etc/bash_completion.d/` as described [here](https://debian-administration.org/article/316/An_introduction_to_bash_completion_part_1) and reset your terminal to use autocompletion. If you make additional annotations to your code, you can get even more intelligent and flexible behavior.
## Creating your own custom functions
## Have the completions code complete your 'nouns'
### Static completion of nouns
This method allows you to provide a pre-defined list of completion choices for your nouns using the `validArgs` field.
For example, if you want `kubectl get [tab][tab]` to show a list of valid "nouns" you have to set them. Simplified code from `kubectl get` looks like:
```go
validArgs []string = { "pod", "node", "service", "replicationcontroller" }
cmd := &cobra.Command{
Use: "get [(-o|--output=)json|yaml|template|...] (RESOURCE [NAME] | RESOURCE/NAME ...)",
Short: "Display one or many resources",
Long: get_long,
Example: get_example,
Run: func(cmd *cobra.Command, args []string) {
err := RunGet(f, out, cmd, args)
util.CheckErr(err)
},
ValidArgs: validArgs,
}
```
Notice we put the "ValidArgs" on the "get" subcommand. Doing so will give results like
```bash
# kubectl get [tab][tab]
node pod replicationcontroller service
```
### Plural form and shortcuts for nouns
If your nouns have a number of aliases, you can define them alongside `ValidArgs` using `ArgAliases`:
```go
argAliases []string = { "pods", "nodes", "services", "svc", "replicationcontrollers", "rc" }
cmd := &cobra.Command{
...
ValidArgs: validArgs,
ArgAliases: argAliases
}
```
The aliases are not shown to the user on tab completion, but they are accepted as valid nouns by
the completion algorithm if entered manually, e.g. in:
```bash
# kubectl get rc [tab][tab]
backend frontend database
```
Note that without declaring `rc` as an alias, the completion algorithm would show the list of nouns
in this example again instead of the replication controllers.
### Dynamic completion of nouns
In some cases it is not possible to provide a list of possible completions in advance. Instead, the list of completions must be determined at execution-time. Cobra provides two ways of defining such dynamic completion of nouns. Note that both these methods can be used along-side each other as long as they are not both used for the same command.
#### 1. Custom completions of nouns written in Go
In a similar fashion as for static completions, you can use the `ValidArgsFunction` field to provide a Go function that Cobra will execute when it needs the list of completion choices for the nouns of a command. Note that either `ValidArgs` or `ValidArgsFunction` can be used for a single cobra command, but not both.
Simplified code from `helm status` looks like:
```go
cmd := &cobra.Command{
Use: "status RELEASE_NAME",
Short: "Display the status of the named release",
Long: status_long,
RunE: func(cmd *cobra.Command, args []string) {
RunGet(args[0])
},
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.BashCompDirective) {
if len(args) != 0 {
return nil, cobra.BashCompDirectiveNoFileComp
}
return getReleasesFromCluster(toComplete), cobra.BashCompDirectiveNoFileComp
},
}
```
Where `getReleasesFromCluster()` is a Go function that obtains the list of current Helm releases running on the Kubernetes cluster.
Notice we put the `ValidArgsFunction` on the `status` subcommand. Let's assume the Helm releases on the cluster are: `harbor`, `notary`, `rook` and `thanos` then this dynamic completion will give results like
```bash
# helm status [tab][tab]
harbor notary rook thanos
```
You may have noticed the use of `cobra.BashCompDirective`. These directives are bit fields allowing to control some shell completion behaviors for your particular completion. You can combine them with the bit-or operator such as `cobra.BashCompDirectiveNoSpace | cobra.BashCompDirectiveNoFileComp`
```go
// Indicates an error occurred and completions should be ignored.
BashCompDirectiveError
// Indicates that the shell should not add a space after the completion,
// even if there is a single completion provided.
BashCompDirectiveNoSpace
// Indicates that the shell should not provide file completion even when
// no completion is provided.
// This currently does not work for zsh or bash < 4
BashCompDirectiveNoFileComp
// Indicates that the shell will perform its default behavior after completions
// have been provided (this implies !BashCompDirectiveNoSpace && !BashCompDirectiveNoFileComp).
BashCompDirectiveDefault
```
When using the `ValidArgsFunction`, Cobra will call your registered function after having parsed all flags and arguments provided in the command-line. You therefore don't need to do this parsing yourself. For example, when a user calls `helm status --namespace my-rook-ns [tab][tab]`, Cobra will call your registered `ValidArgsFunction` after having parsed the `--namespace` flag, as it would have done when calling the `RunE` function.
##### Debugging
Cobra achieves dynamic completions written in Go through the use of a hidden command called by the completion script. To debug your Go completion code, you can call this hidden command directly:
```bash
# helm __complete status har<ENTER>
harbor
:4
Completion ended with directive: BashCompDirectiveNoFileComp # This is on stderr
```
***Important:*** If the noun to complete is empty, you must pass an empty parameter to the `__complete` command:
```bash
# helm __complete status ""<ENTER>
harbor
notary
rook
thanos
:4
Completion ended with directive: BashCompDirectiveNoFileComp # This is on stderr
```
Calling the `__complete` command directly allows you to run the Go debugger to troubleshoot your code. You can also add printouts to your code; Cobra provides the following functions to use for printouts in Go completion code:
```go
// Prints to the completion script debug file (if BASH_COMP_DEBUG_FILE
// is set to a file path) and optionally prints to stderr.
cobra.CompDebug(msg string, printToStdErr bool) {
cobra.CompDebugln(msg string, printToStdErr bool)
// Prints to the completion script debug file (if BASH_COMP_DEBUG_FILE
// is set to a file path) and to stderr.
cobra.CompError(msg string)
cobra.CompErrorln(msg string)
```
***Important:*** You should **not** leave traces that print to stdout in your completion code as they will be interpreted as completion choices by the completion script. Instead, use the cobra-provided debugging traces functions mentioned above.
#### 2. Custom completions of nouns written in Bash
This method allows you to inject bash functions into the completion script. Those bash functions are responsible for providing the completion choices for your own completions.
Some more actual code that works in kubernetes:
@ -111,58 +251,6 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`,
The `BashCompletionFunction` option is really only valid/useful on the root command. Doing the above will cause `__kubectl_custom_func()` (`__<command-use>_custom_func()`) to be called when the built in processor was unable to find a solution. In the case of kubernetes a valid command might look something like `kubectl get pod [mypod]`. If you type `kubectl get pod [tab][tab]` the `__kubectl_customc_func()` will run because the cobra.Command only understood "kubectl" and "get." `__kubectl_custom_func()` will see that the cobra.Command is "kubectl_get" and will thus call another helper `__kubectl_get_resource()`. `__kubectl_get_resource` will look at the 'nouns' collected. In our example the only noun will be `pod`. So it will call `__kubectl_parse_get pod`. `__kubectl_parse_get` will actually call out to kubernetes and get any pods. It will then set `COMPREPLY` to valid pods!
## Have the completions code complete your 'nouns'
In the above example "pod" was assumed to already be typed. But if you want `kubectl get [tab][tab]` to show a list of valid "nouns" you have to set them. Simplified code from `kubectl get` looks like:
```go
validArgs []string = { "pod", "node", "service", "replicationcontroller" }
cmd := &cobra.Command{
Use: "get [(-o|--output=)json|yaml|template|...] (RESOURCE [NAME] | RESOURCE/NAME ...)",
Short: "Display one or many resources",
Long: get_long,
Example: get_example,
Run: func(cmd *cobra.Command, args []string) {
err := RunGet(f, out, cmd, args)
util.CheckErr(err)
},
ValidArgs: validArgs,
}
```
Notice we put the "ValidArgs" on the "get" subcommand. Doing so will give results like
```bash
# kubectl get [tab][tab]
node pod replicationcontroller service
```
## Plural form and shortcuts for nouns
If your nouns have a number of aliases, you can define them alongside `ValidArgs` using `ArgAliases`:
```go
argAliases []string = { "pods", "nodes", "services", "svc", "replicationcontrollers", "rc" }
cmd := &cobra.Command{
...
ValidArgs: validArgs,
ArgAliases: argAliases
}
```
The aliases are not shown to the user on tab completion, but they are accepted as valid nouns by
the completion algorithm if entered manually, e.g. in:
```bash
# kubectl get rc [tab][tab]
backend frontend database
```
Note that without declaring `rc` as an alias, the completion algorithm would show the list of nouns
in this example again instead of the replication controllers.
## Mark flags as required
Most of the time completions will only show subcommands. But if a flag is required to make a subcommand work, you probably want it to show up when the user types [tab][tab]. Marking a flag as 'Required' is incredibly easy.
@ -211,8 +299,43 @@ So while there are many other files in the CWD it only shows me subdirs and thos
# Specify custom flag completion
Similar to the filename completion and filtering using cobra.BashCompFilenameExt, you can specify
a custom flag completion function with cobra.BashCompCustom:
As for nouns, Cobra provides two ways of defining dynamic completion of flags. Note that both these methods can be used along-side each other as long as they are not both used for the same flag.
## 1. Custom completions of flags written in Go
To provide a Go function that Cobra will execute when it needs the list of completion choices for a flag, you must register the function in the following manner:
```go
flagName := "output"
cmd.RegisterFlagCompletionFunc(flagName, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.BashCompDirective) {
return []string{"json", "table", "yaml"}, cobra.BashCompDirectiveDefault
})
```
Notice that calling `RegisterFlagCompletionFunc()` is done through the `command` with which the flag is associated. In our example this dynamic completion will give results like so:
```bash
# helm status --output [tab][tab]
json table yaml
```
### Debugging
You can also easily debug your Go completion code for flags:
```bash
# helm __complete status --output ""
json
table
yaml
:4
Completion ended with directive: BashCompDirectiveNoFileComp # This is on stderr
```
***Important:*** You should **not** leave traces that print to stdout in your completion code as they will be interpreted as completion choices by the completion script. Instead, use the cobra-provided debugging traces functions mentioned in the above section.
## 2. Custom completions of flags written in Bash
Alternatively, you can use bash code for flag custom completion. Similar to the filename
completion and filtering using `cobra.BashCompFilenameExt`, you can specify
a custom flag completion bash function with `cobra.BashCompCustom`:
```go
annotation := make(map[string][]string)
@ -226,7 +349,7 @@ a custom flag completion function with cobra.BashCompCustom:
cmd.Flags().AddFlag(flag)
```
In addition add the `__handle_namespace_flag` implementation in the `BashCompletionFunction`
In addition add the `__kubectl_get_namespaces` implementation in the `BashCompletionFunction`
value, e.g.:
```bash

View file

@ -57,6 +57,10 @@ type Command struct {
// ValidArgs is list of all valid non-flag arguments that are accepted in bash completions
ValidArgs []string
// ValidArgsFunction is an optional function that provides valid non-flag arguments for bash completion.
// It is a dynamic version of using ValidArgs.
// Only one of ValidArgs and ValidArgsFunction can be used for a command.
ValidArgsFunction func(cmd *Command, args []string, toComplete string) ([]string, BashCompDirective)
// Expected arguments
Args PositionalArgs
@ -911,6 +915,9 @@ func (c *Command) ExecuteC() (cmd *Command, err error) {
args = os.Args[1:]
}
// initialize the hidden command to be used for bash completion
c.initCompleteCmd(args)
var flags []string
if c.TraverseChildren {
cmd, flags, err = c.Traverse(args)

299
custom_completions.go Normal file
View file

@ -0,0 +1,299 @@
package cobra
import (
"errors"
"fmt"
"os"
"strings"
"github.com/spf13/pflag"
)
// CompRequestCmd is the name of the hidden command that is used to request
// completion results from the program. It is used by the shell completion script.
const CompRequestCmd = "__complete"
// Global map of flag completion functions.
var flagCompletionFunctions = map[*pflag.Flag]func(cmd *Command, args []string, toComplete string) ([]string, BashCompDirective){}
// BashCompDirective is a bit map representing the different behaviors the shell
// can be instructed to have once completions have been provided.
type BashCompDirective int
const (
// BashCompDirectiveError indicates an error occurred and completions should be ignored.
BashCompDirectiveError BashCompDirective = 1 << iota
// BashCompDirectiveNoSpace indicates that the shell should not add a space
// after the completion even if there is a single completion provided.
BashCompDirectiveNoSpace
// BashCompDirectiveNoFileComp indicates that the shell should not provide
// file completion even when no completion is provided.
// This currently does not work for zsh or bash < 4
BashCompDirectiveNoFileComp
// BashCompDirectiveDefault indicates to let the shell perform its default
// behavior after completions have been provided.
BashCompDirectiveDefault BashCompDirective = 0
)
// RegisterFlagCompletionFunc should be called to register a function to provide completion for a flag.
func (c *Command) RegisterFlagCompletionFunc(flagName string, f func(cmd *Command, args []string, toComplete string) ([]string, BashCompDirective)) error {
flag := c.Flag(flagName)
if flag == nil {
return fmt.Errorf("RegisterFlagCompletionFunc: flag '%s' does not exist", flagName)
}
if _, exists := flagCompletionFunctions[flag]; exists {
return fmt.Errorf("RegisterFlagCompletionFunc: flag '%s' already registered", flagName)
}
flagCompletionFunctions[flag] = f
return nil
}
// Returns a string listing the different directive enabled in the specified parameter
func (d BashCompDirective) string() string {
var directives []string
if d&BashCompDirectiveError != 0 {
directives = append(directives, "BashCompDirectiveError")
}
if d&BashCompDirectiveNoSpace != 0 {
directives = append(directives, "BashCompDirectiveNoSpace")
}
if d&BashCompDirectiveNoFileComp != 0 {
directives = append(directives, "BashCompDirectiveNoFileComp")
}
if len(directives) == 0 {
directives = append(directives, "BashCompDirectiveDefault")
}
if d > BashCompDirectiveError+BashCompDirectiveNoSpace+BashCompDirectiveNoFileComp {
return fmt.Sprintf("ERROR: unexpected BashCompDirective value: %d", d)
}
return strings.Join(directives, ", ")
}
// 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]", CompRequestCmd),
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.", CompRequestCmd),
Run: func(cmd *Command, args []string) {
finalCmd, completions, directive, err := cmd.getCompletions(args)
if err != nil {
CompErrorln(err.Error())
// Keep going for multiple reasons:
// 1- There could be some valid completions even though there was an error
// 2- Even without completions, we need to print the directive
}
for _, comp := range completions {
// Print each possible completion to stdout for the completion script to consume.
fmt.Fprintln(finalCmd.OutOrStdout(), comp)
}
if directive > BashCompDirectiveError+BashCompDirectiveNoSpace+BashCompDirectiveNoFileComp {
directive = BashCompDirectiveDefault
}
// As the last printout, print the completion directive for the completion script to parse.
// The directive integer must be that last character following a single colon (:).
// The completion script expects :<directive>
fmt.Fprintf(finalCmd.OutOrStdout(), ":%d\n", directive)
// Print some helpful info to stderr for the user to understand.
// Output from stderr must be ignored by the completion script.
fmt.Fprintf(finalCmd.ErrOrStderr(), "Completion ended with directive: %s\n", directive.string())
},
}
c.AddCommand(completeCmd)
subCmd, _, err := c.Find(args)
if err != nil || subCmd.Name() != CompRequestCmd {
// Only create this special command if it is actually being called.
// This reduces possible side-effects of creating such a command;
// for example, having this command would cause problems to a
// cobra program that only consists of the root command, since this
// command would cause the root command to suddenly have a subcommand.
c.RemoveCommand(completeCmd)
}
}
func (c *Command) getCompletions(args []string) (*Command, []string, BashCompDirective, error) {
var completions []string
// The last argument, which is not completely typed by the user,
// should not be part of the list of arguments
toComplete := args[len(args)-1]
trimmedArgs := args[:len(args)-1]
// Find the real command for which completion must be performed
finalCmd, finalArgs, err := c.Root().Find(trimmedArgs)
if err != nil {
// Unable to find the real command. E.g., <program> someInvalidCmd <TAB>
return c, completions, BashCompDirectiveDefault, fmt.Errorf("Unable to find a command for arguments: %v", trimmedArgs)
}
var flag *pflag.Flag
if !finalCmd.DisableFlagParsing {
// We only do flag completion if we are allowed to parse flags
// This is important for commands which have requested to do their own flag completion.
flag, finalArgs, toComplete, err = checkIfFlagCompletion(finalCmd, finalArgs, toComplete)
if err != nil {
// Error while attempting to parse flags
return finalCmd, completions, BashCompDirectiveDefault, err
}
}
// Parse the flags and extract the arguments to prepare for calling the completion function
if err = finalCmd.ParseFlags(finalArgs); err != nil {
return finalCmd, completions, BashCompDirectiveDefault, fmt.Errorf("Error while parsing flags from args %v: %s", finalArgs, err.Error())
}
// We only remove the flags from the arguments if DisableFlagParsing is not set.
// This is important for commands which have requested to do their own flag completion.
if !finalCmd.DisableFlagParsing {
finalArgs = finalCmd.Flags().Args()
}
// Find the completion function for the flag or command
var completionFn func(cmd *Command, args []string, toComplete string) ([]string, BashCompDirective)
if flag != nil {
completionFn = flagCompletionFunctions[flag]
} else {
completionFn = finalCmd.ValidArgsFunction
}
if completionFn == nil {
// Go custom completion not supported/needed for this flag or command
return finalCmd, completions, BashCompDirectiveDefault, nil
}
// Call the registered completion function to get the completions
comps, directive := completionFn(finalCmd, finalArgs, toComplete)
completions = append(completions, comps...)
return finalCmd, completions, directive, nil
}
func checkIfFlagCompletion(finalCmd *Command, args []string, lastArg string) (*pflag.Flag, []string, string, error) {
var flagName string
trimmedArgs := args
flagWithEqual := false
if isFlagArg(lastArg) {
if index := strings.Index(lastArg, "="); index >= 0 {
flagName = strings.TrimLeft(lastArg[:index], "-")
lastArg = lastArg[index+1:]
flagWithEqual = true
} else {
return nil, nil, "", errors.New("Unexpected completion request for flag")
}
}
if len(flagName) == 0 {
if len(args) > 0 {
prevArg := args[len(args)-1]
if isFlagArg(prevArg) {
// Only consider the case where the flag does not contain an =.
// If the flag contains an = it means it has already been fully processed,
// so we don't need to deal with it here.
if index := strings.Index(prevArg, "="); index < 0 {
flagName = strings.TrimLeft(prevArg, "-")
// Remove the uncompleted flag or else there could be an error created
// for an invalid value for that flag
trimmedArgs = args[:len(args)-1]
}
}
}
}
if len(flagName) == 0 {
// Not doing flag completion
return nil, trimmedArgs, lastArg, nil
}
flag := findFlag(finalCmd, flagName)
if flag == nil {
// Flag not supported by this command, nothing to complete
err := fmt.Errorf("Subcommand '%s' does not support flag '%s'", finalCmd.Name(), flagName)
return nil, nil, "", err
}
if !flagWithEqual {
if len(flag.NoOptDefVal) != 0 {
// We had assumed dealing with a two-word flag but the flag is a boolean flag.
// In that case, there is no value following it, so we are not really doing flag completion.
// Reset everything to do noun completion.
trimmedArgs = args
flag = nil
}
}
return flag, trimmedArgs, lastArg, nil
}
func findFlag(cmd *Command, name string) *pflag.Flag {
flagSet := cmd.Flags()
if len(name) == 1 {
// First convert the short flag into a long flag
// as the cmd.Flag() search only accepts long flags
if short := flagSet.ShorthandLookup(name); short != nil {
name = short.Name
} else {
set := cmd.InheritedFlags()
if short = set.ShorthandLookup(name); short != nil {
name = short.Name
} else {
return nil
}
}
}
return cmd.Flag(name)
}
// CompDebug prints the specified string to the same file as where the
// completion script prints its logs.
// Note that completion printouts should never be on stdout as they would
// be wrongly interpreted as actual completion choices by the completion script.
func CompDebug(msg string, printToStdErr bool) {
msg = fmt.Sprintf("[Debug] %s", msg)
// Such logs are only printed when the user has set the environment
// variable BASH_COMP_DEBUG_FILE to the path of some file to be used.
if path := os.Getenv("BASH_COMP_DEBUG_FILE"); path != "" {
f, err := os.OpenFile(path,
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err == nil {
defer f.Close()
f.WriteString(msg)
}
}
if printToStdErr {
// Must print to stderr for this not to be read by the completion script.
fmt.Fprintf(os.Stderr, msg)
}
}
// CompDebugln prints the specified string with a newline at the end
// to the same file as where the completion script prints its logs.
// Such logs are only printed when the user has set the environment
// variable BASH_COMP_DEBUG_FILE to the path of some file to be used.
func CompDebugln(msg string, printToStdErr bool) {
CompDebug(fmt.Sprintf("%s\n", msg), printToStdErr)
}
// CompError prints the specified completion message to stderr.
func CompError(msg string) {
msg = fmt.Sprintf("[Error] %s", msg)
CompDebug(msg, true)
}
// CompErrorln prints the specified completion message to stderr with a newline at the end.
func CompErrorln(msg string) {
CompError(fmt.Sprintf("%s\n", msg))
}

385
custom_completions_test.go Normal file
View file

@ -0,0 +1,385 @@
package cobra
import (
"bytes"
"strings"
"testing"
)
func validArgsFunc(cmd *Command, args []string, toComplete string) ([]string, BashCompDirective) {
if len(args) != 0 {
return nil, BashCompDirectiveNoFileComp
}
var completions []string
for _, comp := range []string{"one", "two"} {
if strings.HasPrefix(comp, toComplete) {
completions = append(completions, comp)
}
}
return completions, BashCompDirectiveDefault
}
func validArgsFunc2(cmd *Command, args []string, toComplete string) ([]string, BashCompDirective) {
if len(args) != 0 {
return nil, BashCompDirectiveNoFileComp
}
var completions []string
for _, comp := range []string{"three", "four"} {
if strings.HasPrefix(comp, toComplete) {
completions = append(completions, comp)
}
}
return completions, BashCompDirectiveDefault
}
func TestValidArgsFuncSingleCmd(t *testing.T) {
rootCmd := &Command{
Use: "root",
ValidArgsFunction: validArgsFunc,
Run: emptyRun,
}
// Test completing an empty string
output, err := executeCommand(rootCmd, CompRequestCmd, "")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
expected := strings.Join([]string{
"one",
"two",
":0",
"Completion ended with directive: BashCompDirectiveDefault", ""}, "\n")
if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}
// Check completing with a prefix
output, err = executeCommand(rootCmd, CompRequestCmd, "t")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
expected = strings.Join([]string{
"two",
":0",
"Completion ended with directive: BashCompDirectiveDefault", ""}, "\n")
if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}
}
func TestValidArgsFuncSingleCmdInvalidArg(t *testing.T) {
rootCmd := &Command{
Use: "root",
// If we don't specify a value for Args, this test fails.
// This is only true for a root command without any subcommands, and is caused
// by the fact that the __complete command becomes a subcommand when there should not be one.
// The problem is in the implementation of legacyArgs().
Args: MinimumNArgs(1),
ValidArgsFunction: validArgsFunc,
Run: emptyRun,
}
// Check completing with wrong number of args
output, err := executeCommand(rootCmd, CompRequestCmd, "unexpectedArg", "t")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
expected := strings.Join([]string{
":4",
"Completion ended with directive: BashCompDirectiveNoFileComp", ""}, "\n")
if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}
}
func TestValidArgsFuncChildCmds(t *testing.T) {
rootCmd := &Command{Use: "root", Args: NoArgs, Run: emptyRun}
child1Cmd := &Command{
Use: "child1",
ValidArgsFunction: validArgsFunc,
Run: emptyRun,
}
child2Cmd := &Command{
Use: "child2",
ValidArgsFunction: validArgsFunc2,
Run: emptyRun,
}
rootCmd.AddCommand(child1Cmd, child2Cmd)
// Test completion of first sub-command with empty argument
output, err := executeCommand(rootCmd, CompRequestCmd, "child1", "")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
expected := strings.Join([]string{
"one",
"two",
":0",
"Completion ended with directive: BashCompDirectiveDefault", ""}, "\n")
if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}
// Test completion of first sub-command with a prefix to complete
output, err = executeCommand(rootCmd, CompRequestCmd, "child1", "t")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
expected = strings.Join([]string{
"two",
":0",
"Completion ended with directive: BashCompDirectiveDefault", ""}, "\n")
if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}
// Check completing with wrong number of args
output, err = executeCommand(rootCmd, CompRequestCmd, "child1", "unexpectedArg", "t")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
expected = strings.Join([]string{
":4",
"Completion ended with directive: BashCompDirectiveNoFileComp", ""}, "\n")
if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}
// Test completion of second sub-command with empty argument
output, err = executeCommand(rootCmd, CompRequestCmd, "child2", "")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
expected = strings.Join([]string{
"three",
"four",
":0",
"Completion ended with directive: BashCompDirectiveDefault", ""}, "\n")
if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}
output, err = executeCommand(rootCmd, CompRequestCmd, "child2", "t")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
expected = strings.Join([]string{
"three",
":0",
"Completion ended with directive: BashCompDirectiveDefault", ""}, "\n")
if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}
// Check completing with wrong number of args
output, err = executeCommand(rootCmd, CompRequestCmd, "child2", "unexpectedArg", "t")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
expected = strings.Join([]string{
":4",
"Completion ended with directive: BashCompDirectiveNoFileComp", ""}, "\n")
if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}
}
func TestValidArgsFuncAliases(t *testing.T) {
rootCmd := &Command{Use: "root", Args: NoArgs, Run: emptyRun}
child := &Command{
Use: "child",
Aliases: []string{"son", "daughter"},
ValidArgsFunction: validArgsFunc,
Run: emptyRun,
}
rootCmd.AddCommand(child)
// Test completion of first sub-command with empty argument
output, err := executeCommand(rootCmd, CompRequestCmd, "son", "")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
expected := strings.Join([]string{
"one",
"two",
":0",
"Completion ended with directive: BashCompDirectiveDefault", ""}, "\n")
if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}
// Test completion of first sub-command with a prefix to complete
output, err = executeCommand(rootCmd, CompRequestCmd, "daughter", "t")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
expected = strings.Join([]string{
"two",
":0",
"Completion ended with directive: BashCompDirectiveDefault", ""}, "\n")
if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}
// Check completing with wrong number of args
output, err = executeCommand(rootCmd, CompRequestCmd, "son", "unexpectedArg", "t")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
expected = strings.Join([]string{
":4",
"Completion ended with directive: BashCompDirectiveNoFileComp", ""}, "\n")
if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}
}
func TestValidArgsFuncInScript(t *testing.T) {
rootCmd := &Command{Use: "root", Args: NoArgs, Run: emptyRun}
child := &Command{
Use: "child",
ValidArgsFunction: validArgsFunc,
Run: emptyRun,
}
rootCmd.AddCommand(child)
buf := new(bytes.Buffer)
rootCmd.GenBashCompletion(buf)
output := buf.String()
check(t, output, "has_completion_function=1")
}
func TestNoValidArgsFuncInScript(t *testing.T) {
rootCmd := &Command{Use: "root", Args: NoArgs, Run: emptyRun}
child := &Command{
Use: "child",
Run: emptyRun,
}
rootCmd.AddCommand(child)
buf := new(bytes.Buffer)
rootCmd.GenBashCompletion(buf)
output := buf.String()
checkOmit(t, output, "has_completion_function=1")
}
func TestFlagCompletionInGo(t *testing.T) {
rootCmd := &Command{
Use: "root",
Run: emptyRun,
}
rootCmd.Flags().IntP("introot", "i", -1, "help message for flag introot")
rootCmd.RegisterFlagCompletionFunc("introot", func(cmd *Command, args []string, toComplete string) ([]string, BashCompDirective) {
completions := []string{}
for _, comp := range []string{"1", "2", "10"} {
if strings.HasPrefix(comp, toComplete) {
completions = append(completions, comp)
}
}
return completions, BashCompDirectiveDefault
})
rootCmd.Flags().String("filename", "", "Enter a filename")
rootCmd.RegisterFlagCompletionFunc("filename", func(cmd *Command, args []string, toComplete string) ([]string, BashCompDirective) {
completions := []string{}
for _, comp := range []string{"file.yaml", "myfile.json", "file.xml"} {
if strings.HasPrefix(comp, toComplete) {
completions = append(completions, comp)
}
}
return completions, BashCompDirectiveNoSpace | BashCompDirectiveNoFileComp
})
// Test completing an empty string
output, err := executeCommand(rootCmd, CompRequestCmd, "--introot", "")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
expected := strings.Join([]string{
"1",
"2",
"10",
":0",
"Completion ended with directive: BashCompDirectiveDefault", ""}, "\n")
if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}
// Check completing with a prefix
output, err = executeCommand(rootCmd, CompRequestCmd, "--introot", "1")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
expected = strings.Join([]string{
"1",
"10",
":0",
"Completion ended with directive: BashCompDirectiveDefault", ""}, "\n")
if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}
// Test completing an empty string
output, err = executeCommand(rootCmd, CompRequestCmd, "--filename", "")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
expected = strings.Join([]string{
"file.yaml",
"myfile.json",
"file.xml",
":6",
"Completion ended with directive: BashCompDirectiveNoSpace, BashCompDirectiveNoFileComp", ""}, "\n")
if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}
// Check completing with a prefix
output, err = executeCommand(rootCmd, CompRequestCmd, "--filename", "f")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
expected = strings.Join([]string{
"file.yaml",
"file.xml",
":6",
"Completion ended with directive: BashCompDirectiveNoSpace, BashCompDirectiveNoFileComp", ""}, "\n")
if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}
}