| // 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]CompletionFunc{} | |
| // 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 | |
| // DefaultShellCompDirective sets the ShellCompDirective that is returned | |
| // if no special directive can be determined | |
| DefaultShellCompDirective *ShellCompDirective | |
| } | |
| func (receiver *CompletionOptions) SetDefaultShellCompDirective(directive ShellCompDirective) { | |
| receiver.DefaultShellCompDirective = &directive | |
| } | |
| // Completion is a string that can be used for completions | |
| // | |
| // two formats are supported: | |
| // - the completion choice | |
| // - the completion choice with a textual description (separated by a TAB). | |
| // | |
| // [CompletionWithDesc] can be used to create a completion string with a textual description. | |
| // | |
| // Note: Go type alias is used to provide a more descriptive name in the documentation, but any string can be used. | |
| type Completion = string | |
| // CompletionFunc is a function that provides completion results. | |
| type CompletionFunc = func(cmd *Command, args []string, toComplete string) ([]Completion, ShellCompDirective) | |
| // CompletionWithDesc returns a [Completion] with a description by using the TAB delimited format. | |
| func CompletionWithDesc(choice string, description string) Completion { | |
| return choice + "\t" + description | |
| } | |
| // NoFileCompletions can be used to disable file completion for commands that should | |
| // not trigger file completions. | |
| // | |
| // This method satisfies [CompletionFunc]. | |
| // It can be used with [Command.RegisterFlagCompletionFunc] and for [Command.ValidArgsFunction]. | |
| func NoFileCompletions(cmd *Command, args []string, toComplete string) ([]Completion, ShellCompDirective) { | |
| return nil, ShellCompDirectiveNoFileComp | |
| } | |
| // FixedCompletions can be used to create a completion function which always | |
| // returns the same results. | |
| // | |
| // This method returns a function that satisfies [CompletionFunc] | |
| // It can be used with [Command.RegisterFlagCompletionFunc] and for [Command.ValidArgsFunction]. | |
| func FixedCompletions(choices []Completion, directive ShellCompDirective) CompletionFunc { | |
| return func(cmd *Command, args []string, toComplete string) ([]Completion, ShellCompDirective) { | |
| return choices, directive | |
| } | |
| } | |
| // RegisterFlagCompletionFunc should be called to register a function to provide completion for a flag. | |
| // | |
| // You can use pre-defined completion functions such as [FixedCompletions] or [NoFileCompletions], | |
| // or you can define your own. | |
| func (c *Command) RegisterFlagCompletionFunc(flagName string, f CompletionFunc) 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) (CompletionFunc, 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) | |
| } | |
| } | |
| // SliceValue is a reduced version of [pflag.SliceValue]. It is used to detect | |
| // flags that accept multiple values and therefore can provide completion | |
| // multiple times. | |
| type SliceValue interface { | |
| // GetSlice returns the flag value list as an array of strings. | |
| GetSlice() []string | |
| } | |
| func (c *Command) getCompletions(args []string) (*Command, []Completion, 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, []Completion{}, 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, []Completion{}, 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, []Completion{}, ShellCompDirectiveDefault, flagErr | |
| } | |
| } | |
| // Look for the --help or --version flags. If they are present, | |
| // there should be no further completions. | |
| if helpOrVersionFlagPresent(finalCmd) { | |
| return finalCmd, []Completion{}, 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, []Completion{}, ShellCompDirectiveFilterDirs, nil | |
| } | |
| } | |
| var completions []Completion | |
| 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) { | |
| _, acceptsMultiple := flag.Value.(SliceValue) | |
| acceptsMultiple = acceptsMultiple || | |
| strings.Contains(flag.Value.Type(), "Slice") || | |
| strings.Contains(flag.Value.Type(), "Array") || | |
| strings.HasPrefix(flag.Value.Type(), "stringTo") | |
| if !flag.Changed || acceptsMultiple { | |
| // 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 | |
| // check current and parent commands for a custom DefaultShellCompDirective | |
| for cmd := finalCmd; cmd != nil; cmd = cmd.parent { | |
| if cmd.CompletionOptions.DefaultShellCompDirective != nil { | |
| directive = *cmd.CompletionOptions.DefaultShellCompDirective | |
| break | |
| } | |
| } | |
| 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, CompletionWithDesc(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 CompletionFunc | |
| 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 []Completion | |
| 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(helpFlagName); helpFlag != nil && | |
| len(helpFlag.Annotations[FlagSetByCobraAnnotation]) > 0 && helpFlag.Changed { | |
| return true | |
| } | |
| return false | |
| } | |
| func getFlagNameCompletions(flag *pflag.Flag, toComplete string) []Completion { | |
| if nonCompletableFlag(flag) { | |
| return []Completion{} | |
| } | |
| var completions []Completion | |
| flagName := "--" + flag.Name | |
| if strings.HasPrefix(flagName, toComplete) { | |
| // Flag without the = | |
| completions = append(completions, CompletionWithDesc(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, CompletionWithDesc(flagName, flag.Usage)) | |
| // } | |
| } | |
| flagName = "-" + flag.Shorthand | |
| if len(flag.Shorthand) > 0 && strings.HasPrefix(flagName, toComplete) { | |
| completions = append(completions, CompletionWithDesc(flagName, flag.Usage)) | |
| } | |
| return completions | |
| } | |
| func completeRequireFlags(finalCmd *Command, toComplete string) []Completion { | |
| var completions []Completion | |
| 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(args ...string) { | |
| if c.CompletionOptions.DisableDefaultCmd { | |
| 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 | |
| // Special case to know if there are sub-commands or not. | |
| hasSubCommands := false | |
| for _, cmd := range c.commands { | |
| if cmd.Name() != ShellCompRequestCmd && cmd.Name() != helpCommandName { | |
| // We found a real sub-command (not 'help' or '__complete') | |
| hasSubCommands = true | |
| break | |
| } | |
| } | |
| 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) | |
| if !hasSubCommands { | |
| // If the 'completion' command will be the only sub-command, | |
| // we only create it if it is actually being called. | |
| // This avoids breaking programs that would suddenly find themselves with | |
| // a subcommand, which would prevent them from accepting arguments. | |
| // We also create the 'completion' command if the user is triggering | |
| // shell completion for it (prog __complete completion '') | |
| subCmd, cmdArgs, err := c.Find(args) | |
| if err != nil || subCmd.Name() != compCmdName && | |
| (subCmd.Name() != ShellCompRequestCmd || len(cmdArgs) <= 1 || cmdArgs[0] != compCmdName) { | |
| // The completion command is not being called or being completed so we remove it. | |
| c.RemoveCommand(completionCmd) | |
| return | |
| } | |
| } | |
| 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) | |
| } | |
| completionCmd.AddCommand(bash, zsh, fish, powershell) | |
| } | |
| 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() | |
| 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 | |
| } |