// 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 : 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., someInvalidCmd 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") { // If the flag is not already present, or if it can be specified multiple times (Array or Slice) // 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(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. // If there is exactly 1 sub-command, it must be the __complete command, so we are looking for the case // where there are *more* than one sub-commands: the _complete command *and* a real sub-command. hasSubCommands := len(c.commands) > 1 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 (other than '__complete'), // 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 _ where 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 // _ where 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_ 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 }