mirror of
https://github.com/spf13/cobra
synced 2024-12-29 15:57:10 +00:00
8cb9728476
Co-authored-by: Marc Khouzam <marc.khouzam@gmail.com>
970 lines
36 KiB
Go
970 lines
36 KiB
Go
// Copyright 2013-2023 The Cobra Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package cobra
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/spf13/pflag"
|
|
)
|
|
|
|
const (
|
|
// ShellCompRequestCmd is the name of the hidden command that is used to request
|
|
// completion results from the program. It is used by the shell completion scripts.
|
|
ShellCompRequestCmd = "__complete"
|
|
// ShellCompNoDescRequestCmd is the name of the hidden command that is used to request
|
|
// completion results without their description. It is used by the shell completion scripts.
|
|
ShellCompNoDescRequestCmd = "__completeNoDesc"
|
|
)
|
|
|
|
// Global map of flag completion functions. Make sure to use flagCompletionMutex before you try to read and write from it.
|
|
var flagCompletionFunctions = map[*pflag.Flag]func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective){}
|
|
|
|
// lock for reading and writing from flagCompletionFunctions
|
|
var flagCompletionMutex = &sync.RWMutex{}
|
|
|
|
// ShellCompDirective is a bit map representing the different behaviors the shell
|
|
// can be instructed to have once completions have been provided.
|
|
type ShellCompDirective int
|
|
|
|
type flagCompError struct {
|
|
subCommand string
|
|
flagName string
|
|
}
|
|
|
|
func (e *flagCompError) Error() string {
|
|
return "Subcommand '" + e.subCommand + "' does not support flag '" + e.flagName + "'"
|
|
}
|
|
|
|
const (
|
|
// ShellCompDirectiveError indicates an error occurred and completions should be ignored.
|
|
ShellCompDirectiveError ShellCompDirective = 1 << iota
|
|
|
|
// ShellCompDirectiveNoSpace indicates that the shell should not add a space
|
|
// after the completion even if there is a single completion provided.
|
|
ShellCompDirectiveNoSpace
|
|
|
|
// ShellCompDirectiveNoFileComp indicates that the shell should not provide
|
|
// file completion even when no completion is provided.
|
|
ShellCompDirectiveNoFileComp
|
|
|
|
// ShellCompDirectiveFilterFileExt indicates that the provided completions
|
|
// should be used as file extension filters.
|
|
// For flags, using Command.MarkFlagFilename() and Command.MarkPersistentFlagFilename()
|
|
// is a shortcut to using this directive explicitly. The BashCompFilenameExt
|
|
// annotation can also be used to obtain the same behavior for flags.
|
|
ShellCompDirectiveFilterFileExt
|
|
|
|
// ShellCompDirectiveFilterDirs indicates that only directory names should
|
|
// be provided in file completion. To request directory names within another
|
|
// directory, the returned completions should specify the directory within
|
|
// which to search. The BashCompSubdirsInDir annotation can be used to
|
|
// obtain the same behavior but only for flags.
|
|
ShellCompDirectiveFilterDirs
|
|
|
|
// ShellCompDirectiveKeepOrder indicates that the shell should preserve the order
|
|
// in which the completions are provided
|
|
ShellCompDirectiveKeepOrder
|
|
|
|
// ===========================================================================
|
|
|
|
// All directives using iota should be above this one.
|
|
// For internal use.
|
|
shellCompDirectiveMaxValue
|
|
|
|
// ShellCompDirectiveDefault indicates to let the shell perform its default
|
|
// behavior after completions have been provided.
|
|
// This one must be last to avoid messing up the iota count.
|
|
ShellCompDirectiveDefault ShellCompDirective = 0
|
|
)
|
|
|
|
const (
|
|
// Constants for the completion command
|
|
compCmdName = "completion"
|
|
compCmdNoDescFlagName = "no-descriptions"
|
|
compCmdNoDescFlagDesc = "disable completion descriptions"
|
|
compCmdNoDescFlagDefault = false
|
|
)
|
|
|
|
// CompletionOptions are the options to control shell completion
|
|
type CompletionOptions struct {
|
|
// DisableDefaultCmd prevents Cobra from creating a default 'completion' command
|
|
DisableDefaultCmd bool
|
|
// DisableNoDescFlag prevents Cobra from creating the '--no-descriptions' flag
|
|
// for shells that support completion descriptions
|
|
DisableNoDescFlag bool
|
|
// DisableDescriptions turns off all completion descriptions for shells
|
|
// that support them
|
|
DisableDescriptions bool
|
|
// HiddenDefaultCmd makes the default 'completion' command hidden
|
|
HiddenDefaultCmd bool
|
|
}
|
|
|
|
// NoFileCompletions can be used to disable file completion for commands that should
|
|
// not trigger file completions.
|
|
func NoFileCompletions(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
|
|
return nil, ShellCompDirectiveNoFileComp
|
|
}
|
|
|
|
// FixedCompletions can be used to create a completion function which always
|
|
// returns the same results.
|
|
func FixedCompletions(choices []string, directive ShellCompDirective) func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
|
|
return func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
|
|
return choices, directive
|
|
}
|
|
}
|
|
|
|
// 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, ShellCompDirective)) error {
|
|
flag := c.Flag(flagName)
|
|
if flag == nil {
|
|
return fmt.Errorf("RegisterFlagCompletionFunc: flag '%s' does not exist", flagName)
|
|
}
|
|
flagCompletionMutex.Lock()
|
|
defer flagCompletionMutex.Unlock()
|
|
|
|
if _, exists := flagCompletionFunctions[flag]; exists {
|
|
return fmt.Errorf("RegisterFlagCompletionFunc: flag '%s' already registered", flagName)
|
|
}
|
|
flagCompletionFunctions[flag] = f
|
|
return nil
|
|
}
|
|
|
|
// GetFlagCompletionFunc returns the completion function for the given flag of the command, if available.
|
|
func (c *Command) GetFlagCompletionFunc(flagName string) (func(*Command, []string, string) ([]string, ShellCompDirective), bool) {
|
|
flag := c.Flag(flagName)
|
|
if flag == nil {
|
|
return nil, false
|
|
}
|
|
|
|
flagCompletionMutex.RLock()
|
|
defer flagCompletionMutex.RUnlock()
|
|
|
|
completionFunc, exists := flagCompletionFunctions[flag]
|
|
return completionFunc, exists
|
|
}
|
|
|
|
// Returns a string listing the different directive enabled in the specified parameter
|
|
func (d ShellCompDirective) string() string {
|
|
var directives []string
|
|
if d&ShellCompDirectiveError != 0 {
|
|
directives = append(directives, "ShellCompDirectiveError")
|
|
}
|
|
if d&ShellCompDirectiveNoSpace != 0 {
|
|
directives = append(directives, "ShellCompDirectiveNoSpace")
|
|
}
|
|
if d&ShellCompDirectiveNoFileComp != 0 {
|
|
directives = append(directives, "ShellCompDirectiveNoFileComp")
|
|
}
|
|
if d&ShellCompDirectiveFilterFileExt != 0 {
|
|
directives = append(directives, "ShellCompDirectiveFilterFileExt")
|
|
}
|
|
if d&ShellCompDirectiveFilterDirs != 0 {
|
|
directives = append(directives, "ShellCompDirectiveFilterDirs")
|
|
}
|
|
if d&ShellCompDirectiveKeepOrder != 0 {
|
|
directives = append(directives, "ShellCompDirectiveKeepOrder")
|
|
}
|
|
if len(directives) == 0 {
|
|
directives = append(directives, "ShellCompDirectiveDefault")
|
|
}
|
|
|
|
if d >= shellCompDirectiveMaxValue {
|
|
return fmt.Sprintf("ERROR: unexpected ShellCompDirective value: %d", d)
|
|
}
|
|
return strings.Join(directives, ", ")
|
|
}
|
|
|
|
// initCompleteCmd adds a special hidden command that can be used to request custom completions.
|
|
func (c *Command) initCompleteCmd(args []string) {
|
|
completeCmd := &Command{
|
|
Use: fmt.Sprintf("%s [command-line]", ShellCompRequestCmd),
|
|
Aliases: []string{ShellCompNoDescRequestCmd},
|
|
DisableFlagsInUseLine: true,
|
|
Hidden: true,
|
|
DisableFlagParsing: true,
|
|
Args: MinimumNArgs(1),
|
|
Short: "Request shell completion choices for the specified command-line",
|
|
Long: fmt.Sprintf("%[2]s is a special command that is used by the shell completion logic\n%[1]s",
|
|
"to request completion choices for the specified command-line.", ShellCompRequestCmd),
|
|
Run: func(cmd *Command, args []string) {
|
|
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
|
|
}
|
|
|
|
noDescriptions := cmd.CalledAs() == ShellCompNoDescRequestCmd
|
|
if !noDescriptions {
|
|
if doDescriptions, err := strconv.ParseBool(getEnvConfig(cmd, configEnvVarSuffixDescriptions)); err == nil {
|
|
noDescriptions = !doDescriptions
|
|
}
|
|
}
|
|
noActiveHelp := GetActiveHelpConfig(finalCmd) == activeHelpGlobalDisable
|
|
out := finalCmd.OutOrStdout()
|
|
for _, comp := range completions {
|
|
if noActiveHelp && strings.HasPrefix(comp, activeHelpMarker) {
|
|
// Remove all activeHelp entries if it's disabled.
|
|
continue
|
|
}
|
|
if noDescriptions {
|
|
// Remove any description that may be included following a tab character.
|
|
comp = strings.SplitN(comp, "\t", 2)[0]
|
|
}
|
|
|
|
// Make sure we only write the first line to the output.
|
|
// This is needed if a description contains a linebreak.
|
|
// Otherwise the shell scripts will interpret the other lines as new flags
|
|
// and could therefore provide a wrong completion.
|
|
comp = strings.SplitN(comp, "\n", 2)[0]
|
|
|
|
// Finally trim the completion. This is especially important to get rid
|
|
// of a trailing tab when there are no description following it.
|
|
// For example, a sub-command without a description should not be completed
|
|
// with a tab at the end (or else zsh will show a -- following it
|
|
// although there is no description).
|
|
comp = strings.TrimSpace(comp)
|
|
|
|
// Print each possible completion to the output for the completion script to consume.
|
|
fmt.Fprintln(out, comp)
|
|
}
|
|
|
|
// 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(out, ":%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() != ShellCompRequestCmd {
|
|
// 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, ShellCompDirective, error) {
|
|
// 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]
|
|
|
|
var finalCmd *Command
|
|
var finalArgs []string
|
|
var err error
|
|
// Find the real command for which completion must be performed
|
|
// check if we need to traverse here to parse local flags on parent commands
|
|
if c.Root().TraverseChildren {
|
|
finalCmd, finalArgs, err = c.Root().Traverse(trimmedArgs)
|
|
} else {
|
|
// For Root commands that don't specify any value for their Args fields, when we call
|
|
// Find(), if those Root commands don't have any sub-commands, they will accept arguments.
|
|
// However, because we have added the __complete sub-command in the current code path, the
|
|
// call to Find() -> legacyArgs() will return an error if there are any arguments.
|
|
// To avoid this, we first remove the __complete command to get back to having no sub-commands.
|
|
rootCmd := c.Root()
|
|
if len(rootCmd.Commands()) == 1 {
|
|
rootCmd.RemoveCommand(c)
|
|
}
|
|
|
|
finalCmd, finalArgs, err = rootCmd.Find(trimmedArgs)
|
|
}
|
|
if err != nil {
|
|
// Unable to find the real command. E.g., <program> someInvalidCmd <TAB>
|
|
return c, []string{}, ShellCompDirectiveDefault, fmt.Errorf("unable to find a command for arguments: %v", trimmedArgs)
|
|
}
|
|
finalCmd.ctx = c.ctx
|
|
|
|
// These flags are normally added when `execute()` is called on `finalCmd`,
|
|
// however, when doing completion, we don't call `finalCmd.execute()`.
|
|
// Let's add the --help and --version flag ourselves but only if the finalCmd
|
|
// has not disabled flag parsing; if flag parsing is disabled, it is up to the
|
|
// finalCmd itself to handle the completion of *all* flags.
|
|
if !finalCmd.DisableFlagParsing {
|
|
finalCmd.InitDefaultHelpFlag()
|
|
finalCmd.InitDefaultVersionFlag()
|
|
}
|
|
|
|
// Check if we are doing flag value completion before parsing the flags.
|
|
// This is important because if we are completing a flag value, we need to also
|
|
// remove the flag name argument from the list of finalArgs or else the parsing
|
|
// could fail due to an invalid value (incomplete) for the flag.
|
|
flag, finalArgs, toComplete, flagErr := checkIfFlagCompletion(finalCmd, finalArgs, toComplete)
|
|
|
|
// Check if interspersed is false or -- was set on a previous arg.
|
|
// This works by counting the arguments. Normally -- is not counted as arg but
|
|
// if -- was already set or interspersed is false and there is already one arg then
|
|
// the extra added -- is counted as arg.
|
|
flagCompletion := true
|
|
_ = finalCmd.ParseFlags(append(finalArgs, "--"))
|
|
newArgCount := finalCmd.Flags().NArg()
|
|
|
|
// Parse the flags early so we can check if required flags are set
|
|
if err = finalCmd.ParseFlags(finalArgs); err != nil {
|
|
return finalCmd, []string{}, ShellCompDirectiveDefault, fmt.Errorf("Error while parsing flags from args %v: %s", finalArgs, err.Error())
|
|
}
|
|
|
|
realArgCount := finalCmd.Flags().NArg()
|
|
if newArgCount > realArgCount {
|
|
// don't do flag completion (see above)
|
|
flagCompletion = false
|
|
}
|
|
// Error while attempting to parse flags
|
|
if flagErr != nil {
|
|
// If error type is flagCompError and we don't want flagCompletion we should ignore the error
|
|
if _, ok := flagErr.(*flagCompError); !(ok && !flagCompletion) {
|
|
return finalCmd, []string{}, ShellCompDirectiveDefault, flagErr
|
|
}
|
|
}
|
|
|
|
// Look for the --help or --version flags. If they are present,
|
|
// there should be no further completions.
|
|
if helpOrVersionFlagPresent(finalCmd) {
|
|
return finalCmd, []string{}, ShellCompDirectiveNoFileComp, nil
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
|
|
if flag != nil && flagCompletion {
|
|
// Check if we are completing a flag value subject to annotations
|
|
if validExts, present := flag.Annotations[BashCompFilenameExt]; present {
|
|
if len(validExts) != 0 {
|
|
// File completion filtered by extensions
|
|
return finalCmd, validExts, ShellCompDirectiveFilterFileExt, nil
|
|
}
|
|
|
|
// The annotation requests simple file completion. There is no reason to do
|
|
// that since it is the default behavior anyway. Let's ignore this annotation
|
|
// in case the program also registered a completion function for this flag.
|
|
// Even though it is a mistake on the program's side, let's be nice when we can.
|
|
}
|
|
|
|
if subDir, present := flag.Annotations[BashCompSubdirsInDir]; present {
|
|
if len(subDir) == 1 {
|
|
// Directory completion from within a directory
|
|
return finalCmd, subDir, ShellCompDirectiveFilterDirs, nil
|
|
}
|
|
// Directory completion
|
|
return finalCmd, []string{}, ShellCompDirectiveFilterDirs, nil
|
|
}
|
|
}
|
|
|
|
var completions []string
|
|
var directive ShellCompDirective
|
|
|
|
// Enforce flag groups before doing flag completions
|
|
finalCmd.enforceFlagGroupsForCompletion()
|
|
|
|
// Note that we want to perform flagname completion even if finalCmd.DisableFlagParsing==true;
|
|
// doing this allows for completion of persistent flag names even for commands that disable flag parsing.
|
|
//
|
|
// When doing completion of a flag name, as soon as an argument starts with
|
|
// a '-' we know it is a flag. We cannot use isFlagArg() here as it requires
|
|
// the flag name to be complete
|
|
if flag == nil && len(toComplete) > 0 && toComplete[0] == '-' && !strings.Contains(toComplete, "=") && flagCompletion {
|
|
// First check for required flags
|
|
completions = completeRequireFlags(finalCmd, toComplete)
|
|
|
|
// If we have not found any required flags, only then can we show regular flags
|
|
if len(completions) == 0 {
|
|
doCompleteFlags := func(flag *pflag.Flag) {
|
|
if !flag.Changed ||
|
|
strings.Contains(flag.Value.Type(), "Slice") ||
|
|
strings.Contains(flag.Value.Type(), "Array") ||
|
|
strings.HasPrefix(flag.Value.Type(), "stringTo") {
|
|
// If the flag is not already present, or if it can be specified multiple times (Array, Slice, or stringTo)
|
|
// we suggest it as a completion
|
|
completions = append(completions, getFlagNameCompletions(flag, toComplete)...)
|
|
}
|
|
}
|
|
|
|
// We cannot use finalCmd.Flags() because we may not have called ParsedFlags() for commands
|
|
// that have set DisableFlagParsing; it is ParseFlags() that merges the inherited and
|
|
// non-inherited flags.
|
|
finalCmd.InheritedFlags().VisitAll(func(flag *pflag.Flag) {
|
|
doCompleteFlags(flag)
|
|
})
|
|
// Try to complete non-inherited flags even if DisableFlagParsing==true.
|
|
// This allows programs to tell Cobra about flags for completion even
|
|
// if the actual parsing of flags is not done by Cobra.
|
|
// For instance, Helm uses this to provide flag name completion for
|
|
// some of its plugins.
|
|
finalCmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) {
|
|
doCompleteFlags(flag)
|
|
})
|
|
}
|
|
|
|
directive = ShellCompDirectiveNoFileComp
|
|
if len(completions) == 1 && strings.HasSuffix(completions[0], "=") {
|
|
// If there is a single completion, the shell usually adds a space
|
|
// after the completion. We don't want that if the flag ends with an =
|
|
directive = ShellCompDirectiveNoSpace
|
|
}
|
|
|
|
if !finalCmd.DisableFlagParsing {
|
|
// If DisableFlagParsing==false, we have completed the flags as known by Cobra;
|
|
// we can return what we found.
|
|
// If DisableFlagParsing==true, Cobra may not be aware of all flags, so we
|
|
// let the logic continue to see if ValidArgsFunction needs to be called.
|
|
return finalCmd, completions, directive, nil
|
|
}
|
|
} else {
|
|
directive = ShellCompDirectiveDefault
|
|
if flag == nil {
|
|
foundLocalNonPersistentFlag := false
|
|
// If TraverseChildren is true on the root command we don't check for
|
|
// local flags because we can use a local flag on a parent command
|
|
if !finalCmd.Root().TraverseChildren {
|
|
// Check if there are any local, non-persistent flags on the command-line
|
|
localNonPersistentFlags := finalCmd.LocalNonPersistentFlags()
|
|
finalCmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) {
|
|
if localNonPersistentFlags.Lookup(flag.Name) != nil && flag.Changed {
|
|
foundLocalNonPersistentFlag = true
|
|
}
|
|
})
|
|
}
|
|
|
|
// Complete subcommand names, including the help command
|
|
if len(finalArgs) == 0 && !foundLocalNonPersistentFlag {
|
|
// We only complete sub-commands if:
|
|
// - there are no arguments on the command-line and
|
|
// - there are no local, non-persistent flags on the command-line or TraverseChildren is true
|
|
for _, subCmd := range finalCmd.Commands() {
|
|
if subCmd.IsAvailableCommand() || subCmd == finalCmd.helpCommand {
|
|
if strings.HasPrefix(subCmd.Name(), toComplete) {
|
|
completions = append(completions, fmt.Sprintf("%s\t%s", subCmd.Name(), subCmd.Short))
|
|
}
|
|
directive = ShellCompDirectiveNoFileComp
|
|
}
|
|
}
|
|
}
|
|
|
|
// Complete required flags even without the '-' prefix
|
|
completions = append(completions, completeRequireFlags(finalCmd, toComplete)...)
|
|
|
|
// Always complete ValidArgs, even if we are completing a subcommand name.
|
|
// This is for commands that have both subcommands and ValidArgs.
|
|
if len(finalCmd.ValidArgs) > 0 {
|
|
if len(finalArgs) == 0 {
|
|
// ValidArgs are only for the first argument
|
|
for _, validArg := range finalCmd.ValidArgs {
|
|
if strings.HasPrefix(validArg, toComplete) {
|
|
completions = append(completions, validArg)
|
|
}
|
|
}
|
|
directive = ShellCompDirectiveNoFileComp
|
|
|
|
// If no completions were found within commands or ValidArgs,
|
|
// see if there are any ArgAliases that should be completed.
|
|
if len(completions) == 0 {
|
|
for _, argAlias := range finalCmd.ArgAliases {
|
|
if strings.HasPrefix(argAlias, toComplete) {
|
|
completions = append(completions, argAlias)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If there are ValidArgs specified (even if they don't match), we stop completion.
|
|
// Only one of ValidArgs or ValidArgsFunction can be used for a single command.
|
|
return finalCmd, completions, directive, nil
|
|
}
|
|
|
|
// Let the logic continue so as to add any ValidArgsFunction completions,
|
|
// even if we already found sub-commands.
|
|
// This is for commands that have subcommands but also specify a ValidArgsFunction.
|
|
}
|
|
}
|
|
|
|
// Find the completion function for the flag or command
|
|
var completionFn func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective)
|
|
if flag != nil && flagCompletion {
|
|
flagCompletionMutex.RLock()
|
|
completionFn = flagCompletionFunctions[flag]
|
|
flagCompletionMutex.RUnlock()
|
|
} else {
|
|
completionFn = finalCmd.ValidArgsFunction
|
|
}
|
|
if completionFn != nil {
|
|
// Go custom completion defined for this flag or command.
|
|
// Call the registered completion function to get the completions.
|
|
var comps []string
|
|
comps, directive = completionFn(finalCmd, finalArgs, toComplete)
|
|
completions = append(completions, comps...)
|
|
}
|
|
|
|
return finalCmd, completions, directive, nil
|
|
}
|
|
|
|
func helpOrVersionFlagPresent(cmd *Command) bool {
|
|
if versionFlag := cmd.Flags().Lookup("version"); versionFlag != nil &&
|
|
len(versionFlag.Annotations[FlagSetByCobraAnnotation]) > 0 && versionFlag.Changed {
|
|
return true
|
|
}
|
|
if helpFlag := cmd.Flags().Lookup("help"); helpFlag != nil &&
|
|
len(helpFlag.Annotations[FlagSetByCobraAnnotation]) > 0 && helpFlag.Changed {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func getFlagNameCompletions(flag *pflag.Flag, toComplete string) []string {
|
|
if nonCompletableFlag(flag) {
|
|
return []string{}
|
|
}
|
|
|
|
var completions []string
|
|
flagName := "--" + flag.Name
|
|
if strings.HasPrefix(flagName, toComplete) {
|
|
// Flag without the =
|
|
completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flag.Usage))
|
|
|
|
// Why suggest both long forms: --flag and --flag= ?
|
|
// This forces the user to *always* have to type either an = or a space after the flag name.
|
|
// Let's be nice and avoid making users have to do that.
|
|
// Since boolean flags and shortname flags don't show the = form, let's go that route and never show it.
|
|
// The = form will still work, we just won't suggest it.
|
|
// This also makes the list of suggested flags shorter as we avoid all the = forms.
|
|
//
|
|
// if len(flag.NoOptDefVal) == 0 {
|
|
// // Flag requires a value, so it can be suffixed with =
|
|
// flagName += "="
|
|
// completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flag.Usage))
|
|
// }
|
|
}
|
|
|
|
flagName = "-" + flag.Shorthand
|
|
if len(flag.Shorthand) > 0 && strings.HasPrefix(flagName, toComplete) {
|
|
completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flag.Usage))
|
|
}
|
|
|
|
return completions
|
|
}
|
|
|
|
func completeRequireFlags(finalCmd *Command, toComplete string) []string {
|
|
var completions []string
|
|
|
|
doCompleteRequiredFlags := func(flag *pflag.Flag) {
|
|
if _, present := flag.Annotations[BashCompOneRequiredFlag]; present {
|
|
if !flag.Changed {
|
|
// If the flag is not already present, we suggest it as a completion
|
|
completions = append(completions, getFlagNameCompletions(flag, toComplete)...)
|
|
}
|
|
}
|
|
}
|
|
|
|
// We cannot use finalCmd.Flags() because we may not have called ParsedFlags() for commands
|
|
// that have set DisableFlagParsing; it is ParseFlags() that merges the inherited and
|
|
// non-inherited flags.
|
|
finalCmd.InheritedFlags().VisitAll(func(flag *pflag.Flag) {
|
|
doCompleteRequiredFlags(flag)
|
|
})
|
|
finalCmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) {
|
|
doCompleteRequiredFlags(flag)
|
|
})
|
|
|
|
return completions
|
|
}
|
|
|
|
func checkIfFlagCompletion(finalCmd *Command, args []string, lastArg string) (*pflag.Flag, []string, string, error) {
|
|
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.
|
|
return nil, args, lastArg, nil
|
|
}
|
|
|
|
var flagName string
|
|
trimmedArgs := args
|
|
flagWithEqual := false
|
|
orgLastArg := lastArg
|
|
|
|
// When doing completion of a flag name, as soon as an argument starts with
|
|
// a '-' we know it is a flag. We cannot use isFlagArg() here as that function
|
|
// requires the flag name to be complete
|
|
if len(lastArg) > 0 && lastArg[0] == '-' {
|
|
if index := strings.Index(lastArg, "="); index >= 0 {
|
|
// Flag with an =
|
|
if strings.HasPrefix(lastArg[:index], "--") {
|
|
// Flag has full name
|
|
flagName = lastArg[2:index]
|
|
} else {
|
|
// Flag is shorthand
|
|
// We have to get the last shorthand flag name
|
|
// e.g. `-asd` => d to provide the correct completion
|
|
// https://github.com/spf13/cobra/issues/1257
|
|
flagName = lastArg[index-1 : index]
|
|
}
|
|
lastArg = lastArg[index+1:]
|
|
flagWithEqual = true
|
|
} else {
|
|
// Normal flag completion
|
|
return nil, args, lastArg, nil
|
|
}
|
|
}
|
|
|
|
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 {
|
|
if strings.HasPrefix(prevArg, "--") {
|
|
// Flag has full name
|
|
flagName = prevArg[2:]
|
|
} else {
|
|
// Flag is shorthand
|
|
// We have to get the last shorthand flag name
|
|
// e.g. `-asd` => d to provide the correct completion
|
|
// https://github.com/spf13/cobra/issues/1257
|
|
flagName = prevArg[len(prevArg)-1:]
|
|
}
|
|
// 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, the interspersed option might be set so return the original args
|
|
return nil, args, orgLastArg, &flagCompError{subCommand: finalCmd.Name(), flagName: flagName}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// InitDefaultCompletionCmd adds a default 'completion' command to c.
|
|
// This function will do nothing if any of the following is true:
|
|
// 1- the feature has been explicitly disabled by the program,
|
|
// 2- c has no subcommands (to avoid creating one),
|
|
// 3- c already has a 'completion' command provided by the program.
|
|
func (c *Command) InitDefaultCompletionCmd() {
|
|
if c.CompletionOptions.DisableDefaultCmd || !c.HasSubCommands() {
|
|
return
|
|
}
|
|
|
|
for _, cmd := range c.commands {
|
|
if cmd.Name() == compCmdName || cmd.HasAlias(compCmdName) {
|
|
// A completion command is already available
|
|
return
|
|
}
|
|
}
|
|
|
|
haveNoDescFlag := !c.CompletionOptions.DisableNoDescFlag && !c.CompletionOptions.DisableDescriptions
|
|
|
|
completionCmd := &Command{
|
|
Use: compCmdName,
|
|
Short: "Generate the autocompletion script for the specified shell",
|
|
Long: fmt.Sprintf(`Generate the autocompletion script for %[1]s for the specified shell.
|
|
See each sub-command's help for details on how to use the generated script.
|
|
`, c.Root().Name()),
|
|
Args: NoArgs,
|
|
ValidArgsFunction: NoFileCompletions,
|
|
Hidden: c.CompletionOptions.HiddenDefaultCmd,
|
|
GroupID: c.completionCommandGroupID,
|
|
}
|
|
c.AddCommand(completionCmd)
|
|
|
|
out := c.OutOrStdout()
|
|
noDesc := c.CompletionOptions.DisableDescriptions
|
|
shortDesc := "Generate the autocompletion script for %s"
|
|
bash := &Command{
|
|
Use: "bash",
|
|
Short: fmt.Sprintf(shortDesc, "bash"),
|
|
Long: fmt.Sprintf(`Generate the autocompletion script for the bash shell.
|
|
|
|
This script depends on the 'bash-completion' package.
|
|
If it is not installed already, you can install it via your OS's package manager.
|
|
|
|
To load completions in your current shell session:
|
|
|
|
source <(%[1]s completion bash)
|
|
|
|
To load completions for every new session, execute once:
|
|
|
|
#### Linux:
|
|
|
|
%[1]s completion bash > /etc/bash_completion.d/%[1]s
|
|
|
|
#### macOS:
|
|
|
|
%[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s
|
|
|
|
You will need to start a new shell for this setup to take effect.
|
|
`, c.Root().Name()),
|
|
Args: NoArgs,
|
|
DisableFlagsInUseLine: true,
|
|
ValidArgsFunction: NoFileCompletions,
|
|
RunE: func(cmd *Command, args []string) error {
|
|
return cmd.Root().GenBashCompletionV2(out, !noDesc)
|
|
},
|
|
}
|
|
if haveNoDescFlag {
|
|
bash.Flags().BoolVar(&noDesc, compCmdNoDescFlagName, compCmdNoDescFlagDefault, compCmdNoDescFlagDesc)
|
|
}
|
|
|
|
zsh := &Command{
|
|
Use: "zsh",
|
|
Short: fmt.Sprintf(shortDesc, "zsh"),
|
|
Long: fmt.Sprintf(`Generate the autocompletion script for the zsh shell.
|
|
|
|
If shell completion is not already enabled in your environment you will need
|
|
to enable it. You can execute the following once:
|
|
|
|
echo "autoload -U compinit; compinit" >> ~/.zshrc
|
|
|
|
To load completions in your current shell session:
|
|
|
|
source <(%[1]s completion zsh)
|
|
|
|
To load completions for every new session, execute once:
|
|
|
|
#### Linux:
|
|
|
|
%[1]s completion zsh > "${fpath[1]}/_%[1]s"
|
|
|
|
#### macOS:
|
|
|
|
%[1]s completion zsh > $(brew --prefix)/share/zsh/site-functions/_%[1]s
|
|
|
|
You will need to start a new shell for this setup to take effect.
|
|
`, c.Root().Name()),
|
|
Args: NoArgs,
|
|
ValidArgsFunction: NoFileCompletions,
|
|
RunE: func(cmd *Command, args []string) error {
|
|
if noDesc {
|
|
return cmd.Root().GenZshCompletionNoDesc(out)
|
|
}
|
|
return cmd.Root().GenZshCompletion(out)
|
|
},
|
|
}
|
|
if haveNoDescFlag {
|
|
zsh.Flags().BoolVar(&noDesc, compCmdNoDescFlagName, compCmdNoDescFlagDefault, compCmdNoDescFlagDesc)
|
|
}
|
|
|
|
fish := &Command{
|
|
Use: "fish",
|
|
Short: fmt.Sprintf(shortDesc, "fish"),
|
|
Long: fmt.Sprintf(`Generate the autocompletion script for the fish shell.
|
|
|
|
To load completions in your current shell session:
|
|
|
|
%[1]s completion fish | source
|
|
|
|
To load completions for every new session, execute once:
|
|
|
|
%[1]s completion fish > ~/.config/fish/completions/%[1]s.fish
|
|
|
|
You will need to start a new shell for this setup to take effect.
|
|
`, c.Root().Name()),
|
|
Args: NoArgs,
|
|
ValidArgsFunction: NoFileCompletions,
|
|
RunE: func(cmd *Command, args []string) error {
|
|
return cmd.Root().GenFishCompletion(out, !noDesc)
|
|
},
|
|
}
|
|
if haveNoDescFlag {
|
|
fish.Flags().BoolVar(&noDesc, compCmdNoDescFlagName, compCmdNoDescFlagDefault, compCmdNoDescFlagDesc)
|
|
}
|
|
|
|
powershell := &Command{
|
|
Use: "powershell",
|
|
Short: fmt.Sprintf(shortDesc, "powershell"),
|
|
Long: fmt.Sprintf(`Generate the autocompletion script for powershell.
|
|
|
|
To load completions in your current shell session:
|
|
|
|
%[1]s completion powershell | Out-String | Invoke-Expression
|
|
|
|
To load completions for every new session, add the output of the above command
|
|
to your powershell profile.
|
|
`, c.Root().Name()),
|
|
Args: NoArgs,
|
|
ValidArgsFunction: NoFileCompletions,
|
|
RunE: func(cmd *Command, args []string) error {
|
|
if noDesc {
|
|
return cmd.Root().GenPowerShellCompletion(out)
|
|
}
|
|
return cmd.Root().GenPowerShellCompletionWithDesc(out)
|
|
},
|
|
}
|
|
if haveNoDescFlag {
|
|
powershell.Flags().BoolVar(&noDesc, compCmdNoDescFlagName, compCmdNoDescFlagDefault, compCmdNoDescFlagDesc)
|
|
}
|
|
|
|
nushell := &Command{
|
|
Use: "nushell",
|
|
Short: fmt.Sprintf(shortDesc, "nushell"),
|
|
Long: fmt.Sprintf(`Generate the autocompletion script for nushell.
|
|
|
|
To configure the Nushell cobra external completer for the first time:
|
|
# 1. Copy the output of the command below:
|
|
> %[1]s completion nushell
|
|
# 2. Edit the nushell config file:
|
|
> config nu
|
|
# 3. Paste above the "let-env config" line.
|
|
# 4. Change the config block's external_completer line to be external_completer: $cobra_completer
|
|
# 5. You will need to start a new shel for this setup to take effect.
|
|
|
|
If you have already setup the cobra external completer for other Cobra-based applications:
|
|
# 1. Edit the nushell config file:
|
|
> config nu
|
|
# 2. Modify the cobra_apps variable to contain this new application:
|
|
> let cobra_apps = [ "othercobraapp", "%[1]s" ]
|
|
# 3. You will need to start a new shell for this setup to take effect.
|
|
`, c.Root().Name()),
|
|
Args: NoArgs,
|
|
ValidArgsFunction: NoFileCompletions,
|
|
RunE: func(cmd *Command, args []string) error {
|
|
return cmd.Root().GenNushellCompletion(out, !noDesc)
|
|
},
|
|
}
|
|
if haveNoDescFlag {
|
|
nushell.Flags().BoolVar(&noDesc, compCmdNoDescFlagName, compCmdNoDescFlagDefault, compCmdNoDescFlagDesc)
|
|
}
|
|
|
|
completionCmd.AddCommand(bash, zsh, fish, powershell, nushell)
|
|
}
|
|
|
|
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, 0o644)
|
|
if err == nil {
|
|
defer f.Close()
|
|
WriteStringAndCheck(f, msg)
|
|
}
|
|
}
|
|
|
|
if printToStdErr {
|
|
// Must print to stderr for this not to be read by the completion script.
|
|
fmt.Fprint(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))
|
|
}
|
|
|
|
// These values should not be changed: users will be using them explicitly.
|
|
const (
|
|
configEnvVarGlobalPrefix = "COBRA"
|
|
configEnvVarSuffixDescriptions = "COMPLETION_DESCRIPTIONS"
|
|
)
|
|
|
|
var configEnvVarPrefixSubstRegexp = regexp.MustCompile(`[^A-Z0-9_]`)
|
|
|
|
// configEnvVar returns the name of the program-specific configuration environment
|
|
// variable. It has the format <PROGRAM>_<SUFFIX> where <PROGRAM> is the name of the
|
|
// root command in upper case, with all non-ASCII-alphanumeric characters replaced by `_`.
|
|
func configEnvVar(name, suffix string) string {
|
|
// This format should not be changed: users will be using it explicitly.
|
|
v := strings.ToUpper(fmt.Sprintf("%s_%s", name, suffix))
|
|
v = configEnvVarPrefixSubstRegexp.ReplaceAllString(v, "_")
|
|
return v
|
|
}
|
|
|
|
// getEnvConfig returns the value of the configuration environment variable
|
|
// <PROGRAM>_<SUFFIX> where <PROGRAM> is the name of the root command in upper
|
|
// case, with all non-ASCII-alphanumeric characters replaced by `_`.
|
|
// If the value is empty or not set, the value of the environment variable
|
|
// COBRA_<SUFFIX> is returned instead.
|
|
func getEnvConfig(cmd *Command, suffix string) string {
|
|
v := os.Getenv(configEnvVar(cmd.Root().Name(), suffix))
|
|
if v == "" {
|
|
v = os.Getenv(configEnvVar(configEnvVarGlobalPrefix, suffix))
|
|
}
|
|
return v
|
|
}
|