...

Source file src/github.com/spf13/cobra/completions.go

Documentation: github.com/spf13/cobra

     1  // Copyright 2013-2023 The Cobra Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package cobra
    16  
    17  import (
    18  	"fmt"
    19  	"os"
    20  	"strings"
    21  	"sync"
    22  
    23  	"github.com/spf13/pflag"
    24  )
    25  
    26  const (
    27  	// ShellCompRequestCmd is the name of the hidden command that is used to request
    28  	// completion results from the program.  It is used by the shell completion scripts.
    29  	ShellCompRequestCmd = "__complete"
    30  	// ShellCompNoDescRequestCmd is the name of the hidden command that is used to request
    31  	// completion results without their description.  It is used by the shell completion scripts.
    32  	ShellCompNoDescRequestCmd = "__completeNoDesc"
    33  )
    34  
    35  // Global map of flag completion functions. Make sure to use flagCompletionMutex before you try to read and write from it.
    36  var flagCompletionFunctions = map[*pflag.Flag]func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective){}
    37  
    38  // lock for reading and writing from flagCompletionFunctions
    39  var flagCompletionMutex = &sync.RWMutex{}
    40  
    41  // ShellCompDirective is a bit map representing the different behaviors the shell
    42  // can be instructed to have once completions have been provided.
    43  type ShellCompDirective int
    44  
    45  type flagCompError struct {
    46  	subCommand string
    47  	flagName   string
    48  }
    49  
    50  func (e *flagCompError) Error() string {
    51  	return "Subcommand '" + e.subCommand + "' does not support flag '" + e.flagName + "'"
    52  }
    53  
    54  const (
    55  	// ShellCompDirectiveError indicates an error occurred and completions should be ignored.
    56  	ShellCompDirectiveError ShellCompDirective = 1 << iota
    57  
    58  	// ShellCompDirectiveNoSpace indicates that the shell should not add a space
    59  	// after the completion even if there is a single completion provided.
    60  	ShellCompDirectiveNoSpace
    61  
    62  	// ShellCompDirectiveNoFileComp indicates that the shell should not provide
    63  	// file completion even when no completion is provided.
    64  	ShellCompDirectiveNoFileComp
    65  
    66  	// ShellCompDirectiveFilterFileExt indicates that the provided completions
    67  	// should be used as file extension filters.
    68  	// For flags, using Command.MarkFlagFilename() and Command.MarkPersistentFlagFilename()
    69  	// is a shortcut to using this directive explicitly.  The BashCompFilenameExt
    70  	// annotation can also be used to obtain the same behavior for flags.
    71  	ShellCompDirectiveFilterFileExt
    72  
    73  	// ShellCompDirectiveFilterDirs indicates that only directory names should
    74  	// be provided in file completion.  To request directory names within another
    75  	// directory, the returned completions should specify the directory within
    76  	// which to search.  The BashCompSubdirsInDir annotation can be used to
    77  	// obtain the same behavior but only for flags.
    78  	ShellCompDirectiveFilterDirs
    79  
    80  	// ShellCompDirectiveKeepOrder indicates that the shell should preserve the order
    81  	// in which the completions are provided
    82  	ShellCompDirectiveKeepOrder
    83  
    84  	// ===========================================================================
    85  
    86  	// All directives using iota should be above this one.
    87  	// For internal use.
    88  	shellCompDirectiveMaxValue
    89  
    90  	// ShellCompDirectiveDefault indicates to let the shell perform its default
    91  	// behavior after completions have been provided.
    92  	// This one must be last to avoid messing up the iota count.
    93  	ShellCompDirectiveDefault ShellCompDirective = 0
    94  )
    95  
    96  const (
    97  	// Constants for the completion command
    98  	compCmdName              = "completion"
    99  	compCmdNoDescFlagName    = "no-descriptions"
   100  	compCmdNoDescFlagDesc    = "disable completion descriptions"
   101  	compCmdNoDescFlagDefault = false
   102  )
   103  
   104  // CompletionOptions are the options to control shell completion
   105  type CompletionOptions struct {
   106  	// DisableDefaultCmd prevents Cobra from creating a default 'completion' command
   107  	DisableDefaultCmd bool
   108  	// DisableNoDescFlag prevents Cobra from creating the '--no-descriptions' flag
   109  	// for shells that support completion descriptions
   110  	DisableNoDescFlag bool
   111  	// DisableDescriptions turns off all completion descriptions for shells
   112  	// that support them
   113  	DisableDescriptions bool
   114  	// HiddenDefaultCmd makes the default 'completion' command hidden
   115  	HiddenDefaultCmd bool
   116  }
   117  
   118  // NoFileCompletions can be used to disable file completion for commands that should
   119  // not trigger file completions.
   120  func NoFileCompletions(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
   121  	return nil, ShellCompDirectiveNoFileComp
   122  }
   123  
   124  // FixedCompletions can be used to create a completion function which always
   125  // returns the same results.
   126  func FixedCompletions(choices []string, directive ShellCompDirective) func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
   127  	return func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
   128  		return choices, directive
   129  	}
   130  }
   131  
   132  // RegisterFlagCompletionFunc should be called to register a function to provide completion for a flag.
   133  func (c *Command) RegisterFlagCompletionFunc(flagName string, f func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective)) error {
   134  	flag := c.Flag(flagName)
   135  	if flag == nil {
   136  		return fmt.Errorf("RegisterFlagCompletionFunc: flag '%s' does not exist", flagName)
   137  	}
   138  	flagCompletionMutex.Lock()
   139  	defer flagCompletionMutex.Unlock()
   140  
   141  	if _, exists := flagCompletionFunctions[flag]; exists {
   142  		return fmt.Errorf("RegisterFlagCompletionFunc: flag '%s' already registered", flagName)
   143  	}
   144  	flagCompletionFunctions[flag] = f
   145  	return nil
   146  }
   147  
   148  // GetFlagCompletionFunc returns the completion function for the given flag of the command, if available.
   149  func (c *Command) GetFlagCompletionFunc(flagName string) (func(*Command, []string, string) ([]string, ShellCompDirective), bool) {
   150  	flag := c.Flag(flagName)
   151  	if flag == nil {
   152  		return nil, false
   153  	}
   154  
   155  	flagCompletionMutex.RLock()
   156  	defer flagCompletionMutex.RUnlock()
   157  
   158  	completionFunc, exists := flagCompletionFunctions[flag]
   159  	return completionFunc, exists
   160  }
   161  
   162  // Returns a string listing the different directive enabled in the specified parameter
   163  func (d ShellCompDirective) string() string {
   164  	var directives []string
   165  	if d&ShellCompDirectiveError != 0 {
   166  		directives = append(directives, "ShellCompDirectiveError")
   167  	}
   168  	if d&ShellCompDirectiveNoSpace != 0 {
   169  		directives = append(directives, "ShellCompDirectiveNoSpace")
   170  	}
   171  	if d&ShellCompDirectiveNoFileComp != 0 {
   172  		directives = append(directives, "ShellCompDirectiveNoFileComp")
   173  	}
   174  	if d&ShellCompDirectiveFilterFileExt != 0 {
   175  		directives = append(directives, "ShellCompDirectiveFilterFileExt")
   176  	}
   177  	if d&ShellCompDirectiveFilterDirs != 0 {
   178  		directives = append(directives, "ShellCompDirectiveFilterDirs")
   179  	}
   180  	if d&ShellCompDirectiveKeepOrder != 0 {
   181  		directives = append(directives, "ShellCompDirectiveKeepOrder")
   182  	}
   183  	if len(directives) == 0 {
   184  		directives = append(directives, "ShellCompDirectiveDefault")
   185  	}
   186  
   187  	if d >= shellCompDirectiveMaxValue {
   188  		return fmt.Sprintf("ERROR: unexpected ShellCompDirective value: %d", d)
   189  	}
   190  	return strings.Join(directives, ", ")
   191  }
   192  
   193  // initCompleteCmd adds a special hidden command that can be used to request custom completions.
   194  func (c *Command) initCompleteCmd(args []string) {
   195  	completeCmd := &Command{
   196  		Use:                   fmt.Sprintf("%s [command-line]", ShellCompRequestCmd),
   197  		Aliases:               []string{ShellCompNoDescRequestCmd},
   198  		DisableFlagsInUseLine: true,
   199  		Hidden:                true,
   200  		DisableFlagParsing:    true,
   201  		Args:                  MinimumNArgs(1),
   202  		Short:                 "Request shell completion choices for the specified command-line",
   203  		Long: fmt.Sprintf("%[2]s is a special command that is used by the shell completion logic\n%[1]s",
   204  			"to request completion choices for the specified command-line.", ShellCompRequestCmd),
   205  		Run: func(cmd *Command, args []string) {
   206  			finalCmd, completions, directive, err := cmd.getCompletions(args)
   207  			if err != nil {
   208  				CompErrorln(err.Error())
   209  				// Keep going for multiple reasons:
   210  				// 1- There could be some valid completions even though there was an error
   211  				// 2- Even without completions, we need to print the directive
   212  			}
   213  
   214  			noDescriptions := (cmd.CalledAs() == ShellCompNoDescRequestCmd)
   215  			for _, comp := range completions {
   216  				if GetActiveHelpConfig(finalCmd) == activeHelpGlobalDisable {
   217  					// Remove all activeHelp entries in this case
   218  					if strings.HasPrefix(comp, activeHelpMarker) {
   219  						continue
   220  					}
   221  				}
   222  				if noDescriptions {
   223  					// Remove any description that may be included following a tab character.
   224  					comp = strings.Split(comp, "\t")[0]
   225  				}
   226  
   227  				// Make sure we only write the first line to the output.
   228  				// This is needed if a description contains a linebreak.
   229  				// Otherwise the shell scripts will interpret the other lines as new flags
   230  				// and could therefore provide a wrong completion.
   231  				comp = strings.Split(comp, "\n")[0]
   232  
   233  				// Finally trim the completion.  This is especially important to get rid
   234  				// of a trailing tab when there are no description following it.
   235  				// For example, a sub-command without a description should not be completed
   236  				// with a tab at the end (or else zsh will show a -- following it
   237  				// although there is no description).
   238  				comp = strings.TrimSpace(comp)
   239  
   240  				// Print each possible completion to stdout for the completion script to consume.
   241  				fmt.Fprintln(finalCmd.OutOrStdout(), comp)
   242  			}
   243  
   244  			// As the last printout, print the completion directive for the completion script to parse.
   245  			// The directive integer must be that last character following a single colon (:).
   246  			// The completion script expects :<directive>
   247  			fmt.Fprintf(finalCmd.OutOrStdout(), ":%d\n", directive)
   248  
   249  			// Print some helpful info to stderr for the user to understand.
   250  			// Output from stderr must be ignored by the completion script.
   251  			fmt.Fprintf(finalCmd.ErrOrStderr(), "Completion ended with directive: %s\n", directive.string())
   252  		},
   253  	}
   254  	c.AddCommand(completeCmd)
   255  	subCmd, _, err := c.Find(args)
   256  	if err != nil || subCmd.Name() != ShellCompRequestCmd {
   257  		// Only create this special command if it is actually being called.
   258  		// This reduces possible side-effects of creating such a command;
   259  		// for example, having this command would cause problems to a
   260  		// cobra program that only consists of the root command, since this
   261  		// command would cause the root command to suddenly have a subcommand.
   262  		c.RemoveCommand(completeCmd)
   263  	}
   264  }
   265  
   266  func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDirective, error) {
   267  	// The last argument, which is not completely typed by the user,
   268  	// should not be part of the list of arguments
   269  	toComplete := args[len(args)-1]
   270  	trimmedArgs := args[:len(args)-1]
   271  
   272  	var finalCmd *Command
   273  	var finalArgs []string
   274  	var err error
   275  	// Find the real command for which completion must be performed
   276  	// check if we need to traverse here to parse local flags on parent commands
   277  	if c.Root().TraverseChildren {
   278  		finalCmd, finalArgs, err = c.Root().Traverse(trimmedArgs)
   279  	} else {
   280  		// For Root commands that don't specify any value for their Args fields, when we call
   281  		// Find(), if those Root commands don't have any sub-commands, they will accept arguments.
   282  		// However, because we have added the __complete sub-command in the current code path, the
   283  		// call to Find() -> legacyArgs() will return an error if there are any arguments.
   284  		// To avoid this, we first remove the __complete command to get back to having no sub-commands.
   285  		rootCmd := c.Root()
   286  		if len(rootCmd.Commands()) == 1 {
   287  			rootCmd.RemoveCommand(c)
   288  		}
   289  
   290  		finalCmd, finalArgs, err = rootCmd.Find(trimmedArgs)
   291  	}
   292  	if err != nil {
   293  		// Unable to find the real command. E.g., <program> someInvalidCmd <TAB>
   294  		return c, []string{}, ShellCompDirectiveDefault, fmt.Errorf("Unable to find a command for arguments: %v", trimmedArgs)
   295  	}
   296  	finalCmd.ctx = c.ctx
   297  
   298  	// These flags are normally added when `execute()` is called on `finalCmd`,
   299  	// however, when doing completion, we don't call `finalCmd.execute()`.
   300  	// Let's add the --help and --version flag ourselves but only if the finalCmd
   301  	// has not disabled flag parsing; if flag parsing is disabled, it is up to the
   302  	// finalCmd itself to handle the completion of *all* flags.
   303  	if !finalCmd.DisableFlagParsing {
   304  		finalCmd.InitDefaultHelpFlag()
   305  		finalCmd.InitDefaultVersionFlag()
   306  	}
   307  
   308  	// Check if we are doing flag value completion before parsing the flags.
   309  	// This is important because if we are completing a flag value, we need to also
   310  	// remove the flag name argument from the list of finalArgs or else the parsing
   311  	// could fail due to an invalid value (incomplete) for the flag.
   312  	flag, finalArgs, toComplete, flagErr := checkIfFlagCompletion(finalCmd, finalArgs, toComplete)
   313  
   314  	// Check if interspersed is false or -- was set on a previous arg.
   315  	// This works by counting the arguments. Normally -- is not counted as arg but
   316  	// if -- was already set or interspersed is false and there is already one arg then
   317  	// the extra added -- is counted as arg.
   318  	flagCompletion := true
   319  	_ = finalCmd.ParseFlags(append(finalArgs, "--"))
   320  	newArgCount := finalCmd.Flags().NArg()
   321  
   322  	// Parse the flags early so we can check if required flags are set
   323  	if err = finalCmd.ParseFlags(finalArgs); err != nil {
   324  		return finalCmd, []string{}, ShellCompDirectiveDefault, fmt.Errorf("Error while parsing flags from args %v: %s", finalArgs, err.Error())
   325  	}
   326  
   327  	realArgCount := finalCmd.Flags().NArg()
   328  	if newArgCount > realArgCount {
   329  		// don't do flag completion (see above)
   330  		flagCompletion = false
   331  	}
   332  	// Error while attempting to parse flags
   333  	if flagErr != nil {
   334  		// If error type is flagCompError and we don't want flagCompletion we should ignore the error
   335  		if _, ok := flagErr.(*flagCompError); !(ok && !flagCompletion) {
   336  			return finalCmd, []string{}, ShellCompDirectiveDefault, flagErr
   337  		}
   338  	}
   339  
   340  	// Look for the --help or --version flags.  If they are present,
   341  	// there should be no further completions.
   342  	if helpOrVersionFlagPresent(finalCmd) {
   343  		return finalCmd, []string{}, ShellCompDirectiveNoFileComp, nil
   344  	}
   345  
   346  	// We only remove the flags from the arguments if DisableFlagParsing is not set.
   347  	// This is important for commands which have requested to do their own flag completion.
   348  	if !finalCmd.DisableFlagParsing {
   349  		finalArgs = finalCmd.Flags().Args()
   350  	}
   351  
   352  	if flag != nil && flagCompletion {
   353  		// Check if we are completing a flag value subject to annotations
   354  		if validExts, present := flag.Annotations[BashCompFilenameExt]; present {
   355  			if len(validExts) != 0 {
   356  				// File completion filtered by extensions
   357  				return finalCmd, validExts, ShellCompDirectiveFilterFileExt, nil
   358  			}
   359  
   360  			// The annotation requests simple file completion.  There is no reason to do
   361  			// that since it is the default behavior anyway.  Let's ignore this annotation
   362  			// in case the program also registered a completion function for this flag.
   363  			// Even though it is a mistake on the program's side, let's be nice when we can.
   364  		}
   365  
   366  		if subDir, present := flag.Annotations[BashCompSubdirsInDir]; present {
   367  			if len(subDir) == 1 {
   368  				// Directory completion from within a directory
   369  				return finalCmd, subDir, ShellCompDirectiveFilterDirs, nil
   370  			}
   371  			// Directory completion
   372  			return finalCmd, []string{}, ShellCompDirectiveFilterDirs, nil
   373  		}
   374  	}
   375  
   376  	var completions []string
   377  	var directive ShellCompDirective
   378  
   379  	// Enforce flag groups before doing flag completions
   380  	finalCmd.enforceFlagGroupsForCompletion()
   381  
   382  	// Note that we want to perform flagname completion even if finalCmd.DisableFlagParsing==true;
   383  	// doing this allows for completion of persistent flag names even for commands that disable flag parsing.
   384  	//
   385  	// When doing completion of a flag name, as soon as an argument starts with
   386  	// a '-' we know it is a flag.  We cannot use isFlagArg() here as it requires
   387  	// the flag name to be complete
   388  	if flag == nil && len(toComplete) > 0 && toComplete[0] == '-' && !strings.Contains(toComplete, "=") && flagCompletion {
   389  		// First check for required flags
   390  		completions = completeRequireFlags(finalCmd, toComplete)
   391  
   392  		// If we have not found any required flags, only then can we show regular flags
   393  		if len(completions) == 0 {
   394  			doCompleteFlags := func(flag *pflag.Flag) {
   395  				if !flag.Changed ||
   396  					strings.Contains(flag.Value.Type(), "Slice") ||
   397  					strings.Contains(flag.Value.Type(), "Array") {
   398  					// If the flag is not already present, or if it can be specified multiple times (Array or Slice)
   399  					// we suggest it as a completion
   400  					completions = append(completions, getFlagNameCompletions(flag, toComplete)...)
   401  				}
   402  			}
   403  
   404  			// We cannot use finalCmd.Flags() because we may not have called ParsedFlags() for commands
   405  			// that have set DisableFlagParsing; it is ParseFlags() that merges the inherited and
   406  			// non-inherited flags.
   407  			finalCmd.InheritedFlags().VisitAll(func(flag *pflag.Flag) {
   408  				doCompleteFlags(flag)
   409  			})
   410  			// Try to complete non-inherited flags even if DisableFlagParsing==true.
   411  			// This allows programs to tell Cobra about flags for completion even
   412  			// if the actual parsing of flags is not done by Cobra.
   413  			// For instance, Helm uses this to provide flag name completion for
   414  			// some of its plugins.
   415  			finalCmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) {
   416  				doCompleteFlags(flag)
   417  			})
   418  		}
   419  
   420  		directive = ShellCompDirectiveNoFileComp
   421  		if len(completions) == 1 && strings.HasSuffix(completions[0], "=") {
   422  			// If there is a single completion, the shell usually adds a space
   423  			// after the completion.  We don't want that if the flag ends with an =
   424  			directive = ShellCompDirectiveNoSpace
   425  		}
   426  
   427  		if !finalCmd.DisableFlagParsing {
   428  			// If DisableFlagParsing==false, we have completed the flags as known by Cobra;
   429  			// we can return what we found.
   430  			// If DisableFlagParsing==true, Cobra may not be aware of all flags, so we
   431  			// let the logic continue to see if ValidArgsFunction needs to be called.
   432  			return finalCmd, completions, directive, nil
   433  		}
   434  	} else {
   435  		directive = ShellCompDirectiveDefault
   436  		if flag == nil {
   437  			foundLocalNonPersistentFlag := false
   438  			// If TraverseChildren is true on the root command we don't check for
   439  			// local flags because we can use a local flag on a parent command
   440  			if !finalCmd.Root().TraverseChildren {
   441  				// Check if there are any local, non-persistent flags on the command-line
   442  				localNonPersistentFlags := finalCmd.LocalNonPersistentFlags()
   443  				finalCmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) {
   444  					if localNonPersistentFlags.Lookup(flag.Name) != nil && flag.Changed {
   445  						foundLocalNonPersistentFlag = true
   446  					}
   447  				})
   448  			}
   449  
   450  			// Complete subcommand names, including the help command
   451  			if len(finalArgs) == 0 && !foundLocalNonPersistentFlag {
   452  				// We only complete sub-commands if:
   453  				// - there are no arguments on the command-line and
   454  				// - there are no local, non-persistent flags on the command-line or TraverseChildren is true
   455  				for _, subCmd := range finalCmd.Commands() {
   456  					if subCmd.IsAvailableCommand() || subCmd == finalCmd.helpCommand {
   457  						if strings.HasPrefix(subCmd.Name(), toComplete) {
   458  							completions = append(completions, fmt.Sprintf("%s\t%s", subCmd.Name(), subCmd.Short))
   459  						}
   460  						directive = ShellCompDirectiveNoFileComp
   461  					}
   462  				}
   463  			}
   464  
   465  			// Complete required flags even without the '-' prefix
   466  			completions = append(completions, completeRequireFlags(finalCmd, toComplete)...)
   467  
   468  			// Always complete ValidArgs, even if we are completing a subcommand name.
   469  			// This is for commands that have both subcommands and ValidArgs.
   470  			if len(finalCmd.ValidArgs) > 0 {
   471  				if len(finalArgs) == 0 {
   472  					// ValidArgs are only for the first argument
   473  					for _, validArg := range finalCmd.ValidArgs {
   474  						if strings.HasPrefix(validArg, toComplete) {
   475  							completions = append(completions, validArg)
   476  						}
   477  					}
   478  					directive = ShellCompDirectiveNoFileComp
   479  
   480  					// If no completions were found within commands or ValidArgs,
   481  					// see if there are any ArgAliases that should be completed.
   482  					if len(completions) == 0 {
   483  						for _, argAlias := range finalCmd.ArgAliases {
   484  							if strings.HasPrefix(argAlias, toComplete) {
   485  								completions = append(completions, argAlias)
   486  							}
   487  						}
   488  					}
   489  				}
   490  
   491  				// If there are ValidArgs specified (even if they don't match), we stop completion.
   492  				// Only one of ValidArgs or ValidArgsFunction can be used for a single command.
   493  				return finalCmd, completions, directive, nil
   494  			}
   495  
   496  			// Let the logic continue so as to add any ValidArgsFunction completions,
   497  			// even if we already found sub-commands.
   498  			// This is for commands that have subcommands but also specify a ValidArgsFunction.
   499  		}
   500  	}
   501  
   502  	// Find the completion function for the flag or command
   503  	var completionFn func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective)
   504  	if flag != nil && flagCompletion {
   505  		flagCompletionMutex.RLock()
   506  		completionFn = flagCompletionFunctions[flag]
   507  		flagCompletionMutex.RUnlock()
   508  	} else {
   509  		completionFn = finalCmd.ValidArgsFunction
   510  	}
   511  	if completionFn != nil {
   512  		// Go custom completion defined for this flag or command.
   513  		// Call the registered completion function to get the completions.
   514  		var comps []string
   515  		comps, directive = completionFn(finalCmd, finalArgs, toComplete)
   516  		completions = append(completions, comps...)
   517  	}
   518  
   519  	return finalCmd, completions, directive, nil
   520  }
   521  
   522  func helpOrVersionFlagPresent(cmd *Command) bool {
   523  	if versionFlag := cmd.Flags().Lookup("version"); versionFlag != nil &&
   524  		len(versionFlag.Annotations[FlagSetByCobraAnnotation]) > 0 && versionFlag.Changed {
   525  		return true
   526  	}
   527  	if helpFlag := cmd.Flags().Lookup("help"); helpFlag != nil &&
   528  		len(helpFlag.Annotations[FlagSetByCobraAnnotation]) > 0 && helpFlag.Changed {
   529  		return true
   530  	}
   531  	return false
   532  }
   533  
   534  func getFlagNameCompletions(flag *pflag.Flag, toComplete string) []string {
   535  	if nonCompletableFlag(flag) {
   536  		return []string{}
   537  	}
   538  
   539  	var completions []string
   540  	flagName := "--" + flag.Name
   541  	if strings.HasPrefix(flagName, toComplete) {
   542  		// Flag without the =
   543  		completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flag.Usage))
   544  
   545  		// Why suggest both long forms: --flag and --flag= ?
   546  		// This forces the user to *always* have to type either an = or a space after the flag name.
   547  		// Let's be nice and avoid making users have to do that.
   548  		// Since boolean flags and shortname flags don't show the = form, let's go that route and never show it.
   549  		// The = form will still work, we just won't suggest it.
   550  		// This also makes the list of suggested flags shorter as we avoid all the = forms.
   551  		//
   552  		// if len(flag.NoOptDefVal) == 0 {
   553  		// 	// Flag requires a value, so it can be suffixed with =
   554  		// 	flagName += "="
   555  		// 	completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flag.Usage))
   556  		// }
   557  	}
   558  
   559  	flagName = "-" + flag.Shorthand
   560  	if len(flag.Shorthand) > 0 && strings.HasPrefix(flagName, toComplete) {
   561  		completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flag.Usage))
   562  	}
   563  
   564  	return completions
   565  }
   566  
   567  func completeRequireFlags(finalCmd *Command, toComplete string) []string {
   568  	var completions []string
   569  
   570  	doCompleteRequiredFlags := func(flag *pflag.Flag) {
   571  		if _, present := flag.Annotations[BashCompOneRequiredFlag]; present {
   572  			if !flag.Changed {
   573  				// If the flag is not already present, we suggest it as a completion
   574  				completions = append(completions, getFlagNameCompletions(flag, toComplete)...)
   575  			}
   576  		}
   577  	}
   578  
   579  	// We cannot use finalCmd.Flags() because we may not have called ParsedFlags() for commands
   580  	// that have set DisableFlagParsing; it is ParseFlags() that merges the inherited and
   581  	// non-inherited flags.
   582  	finalCmd.InheritedFlags().VisitAll(func(flag *pflag.Flag) {
   583  		doCompleteRequiredFlags(flag)
   584  	})
   585  	finalCmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) {
   586  		doCompleteRequiredFlags(flag)
   587  	})
   588  
   589  	return completions
   590  }
   591  
   592  func checkIfFlagCompletion(finalCmd *Command, args []string, lastArg string) (*pflag.Flag, []string, string, error) {
   593  	if finalCmd.DisableFlagParsing {
   594  		// We only do flag completion if we are allowed to parse flags
   595  		// This is important for commands which have requested to do their own flag completion.
   596  		return nil, args, lastArg, nil
   597  	}
   598  
   599  	var flagName string
   600  	trimmedArgs := args
   601  	flagWithEqual := false
   602  	orgLastArg := lastArg
   603  
   604  	// When doing completion of a flag name, as soon as an argument starts with
   605  	// a '-' we know it is a flag.  We cannot use isFlagArg() here as that function
   606  	// requires the flag name to be complete
   607  	if len(lastArg) > 0 && lastArg[0] == '-' {
   608  		if index := strings.Index(lastArg, "="); index >= 0 {
   609  			// Flag with an =
   610  			if strings.HasPrefix(lastArg[:index], "--") {
   611  				// Flag has full name
   612  				flagName = lastArg[2:index]
   613  			} else {
   614  				// Flag is shorthand
   615  				// We have to get the last shorthand flag name
   616  				// e.g. `-asd` => d to provide the correct completion
   617  				// https://github.com/spf13/cobra/issues/1257
   618  				flagName = lastArg[index-1 : index]
   619  			}
   620  			lastArg = lastArg[index+1:]
   621  			flagWithEqual = true
   622  		} else {
   623  			// Normal flag completion
   624  			return nil, args, lastArg, nil
   625  		}
   626  	}
   627  
   628  	if len(flagName) == 0 {
   629  		if len(args) > 0 {
   630  			prevArg := args[len(args)-1]
   631  			if isFlagArg(prevArg) {
   632  				// Only consider the case where the flag does not contain an =.
   633  				// If the flag contains an = it means it has already been fully processed,
   634  				// so we don't need to deal with it here.
   635  				if index := strings.Index(prevArg, "="); index < 0 {
   636  					if strings.HasPrefix(prevArg, "--") {
   637  						// Flag has full name
   638  						flagName = prevArg[2:]
   639  					} else {
   640  						// Flag is shorthand
   641  						// We have to get the last shorthand flag name
   642  						// e.g. `-asd` => d to provide the correct completion
   643  						// https://github.com/spf13/cobra/issues/1257
   644  						flagName = prevArg[len(prevArg)-1:]
   645  					}
   646  					// Remove the uncompleted flag or else there could be an error created
   647  					// for an invalid value for that flag
   648  					trimmedArgs = args[:len(args)-1]
   649  				}
   650  			}
   651  		}
   652  	}
   653  
   654  	if len(flagName) == 0 {
   655  		// Not doing flag completion
   656  		return nil, trimmedArgs, lastArg, nil
   657  	}
   658  
   659  	flag := findFlag(finalCmd, flagName)
   660  	if flag == nil {
   661  		// Flag not supported by this command, the interspersed option might be set so return the original args
   662  		return nil, args, orgLastArg, &flagCompError{subCommand: finalCmd.Name(), flagName: flagName}
   663  	}
   664  
   665  	if !flagWithEqual {
   666  		if len(flag.NoOptDefVal) != 0 {
   667  			// We had assumed dealing with a two-word flag but the flag is a boolean flag.
   668  			// In that case, there is no value following it, so we are not really doing flag completion.
   669  			// Reset everything to do noun completion.
   670  			trimmedArgs = args
   671  			flag = nil
   672  		}
   673  	}
   674  
   675  	return flag, trimmedArgs, lastArg, nil
   676  }
   677  
   678  // InitDefaultCompletionCmd adds a default 'completion' command to c.
   679  // This function will do nothing if any of the following is true:
   680  // 1- the feature has been explicitly disabled by the program,
   681  // 2- c has no subcommands (to avoid creating one),
   682  // 3- c already has a 'completion' command provided by the program.
   683  func (c *Command) InitDefaultCompletionCmd() {
   684  	if c.CompletionOptions.DisableDefaultCmd || !c.HasSubCommands() {
   685  		return
   686  	}
   687  
   688  	for _, cmd := range c.commands {
   689  		if cmd.Name() == compCmdName || cmd.HasAlias(compCmdName) {
   690  			// A completion command is already available
   691  			return
   692  		}
   693  	}
   694  
   695  	haveNoDescFlag := !c.CompletionOptions.DisableNoDescFlag && !c.CompletionOptions.DisableDescriptions
   696  
   697  	completionCmd := &Command{
   698  		Use:   compCmdName,
   699  		Short: "Generate the autocompletion script for the specified shell",
   700  		Long: fmt.Sprintf(`Generate the autocompletion script for %[1]s for the specified shell.
   701  See each sub-command's help for details on how to use the generated script.
   702  `, c.Root().Name()),
   703  		Args:              NoArgs,
   704  		ValidArgsFunction: NoFileCompletions,
   705  		Hidden:            c.CompletionOptions.HiddenDefaultCmd,
   706  		GroupID:           c.completionCommandGroupID,
   707  	}
   708  	c.AddCommand(completionCmd)
   709  
   710  	out := c.OutOrStdout()
   711  	noDesc := c.CompletionOptions.DisableDescriptions
   712  	shortDesc := "Generate the autocompletion script for %s"
   713  	bash := &Command{
   714  		Use:   "bash",
   715  		Short: fmt.Sprintf(shortDesc, "bash"),
   716  		Long: fmt.Sprintf(`Generate the autocompletion script for the bash shell.
   717  
   718  This script depends on the 'bash-completion' package.
   719  If it is not installed already, you can install it via your OS's package manager.
   720  
   721  To load completions in your current shell session:
   722  
   723  	source <(%[1]s completion bash)
   724  
   725  To load completions for every new session, execute once:
   726  
   727  #### Linux:
   728  
   729  	%[1]s completion bash > /etc/bash_completion.d/%[1]s
   730  
   731  #### macOS:
   732  
   733  	%[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s
   734  
   735  You will need to start a new shell for this setup to take effect.
   736  `, c.Root().Name()),
   737  		Args:                  NoArgs,
   738  		DisableFlagsInUseLine: true,
   739  		ValidArgsFunction:     NoFileCompletions,
   740  		RunE: func(cmd *Command, args []string) error {
   741  			return cmd.Root().GenBashCompletionV2(out, !noDesc)
   742  		},
   743  	}
   744  	if haveNoDescFlag {
   745  		bash.Flags().BoolVar(&noDesc, compCmdNoDescFlagName, compCmdNoDescFlagDefault, compCmdNoDescFlagDesc)
   746  	}
   747  
   748  	zsh := &Command{
   749  		Use:   "zsh",
   750  		Short: fmt.Sprintf(shortDesc, "zsh"),
   751  		Long: fmt.Sprintf(`Generate the autocompletion script for the zsh shell.
   752  
   753  If shell completion is not already enabled in your environment you will need
   754  to enable it.  You can execute the following once:
   755  
   756  	echo "autoload -U compinit; compinit" >> ~/.zshrc
   757  
   758  To load completions in your current shell session:
   759  
   760  	source <(%[1]s completion zsh)
   761  
   762  To load completions for every new session, execute once:
   763  
   764  #### Linux:
   765  
   766  	%[1]s completion zsh > "${fpath[1]}/_%[1]s"
   767  
   768  #### macOS:
   769  
   770  	%[1]s completion zsh > $(brew --prefix)/share/zsh/site-functions/_%[1]s
   771  
   772  You will need to start a new shell for this setup to take effect.
   773  `, c.Root().Name()),
   774  		Args:              NoArgs,
   775  		ValidArgsFunction: NoFileCompletions,
   776  		RunE: func(cmd *Command, args []string) error {
   777  			if noDesc {
   778  				return cmd.Root().GenZshCompletionNoDesc(out)
   779  			}
   780  			return cmd.Root().GenZshCompletion(out)
   781  		},
   782  	}
   783  	if haveNoDescFlag {
   784  		zsh.Flags().BoolVar(&noDesc, compCmdNoDescFlagName, compCmdNoDescFlagDefault, compCmdNoDescFlagDesc)
   785  	}
   786  
   787  	fish := &Command{
   788  		Use:   "fish",
   789  		Short: fmt.Sprintf(shortDesc, "fish"),
   790  		Long: fmt.Sprintf(`Generate the autocompletion script for the fish shell.
   791  
   792  To load completions in your current shell session:
   793  
   794  	%[1]s completion fish | source
   795  
   796  To load completions for every new session, execute once:
   797  
   798  	%[1]s completion fish > ~/.config/fish/completions/%[1]s.fish
   799  
   800  You will need to start a new shell for this setup to take effect.
   801  `, c.Root().Name()),
   802  		Args:              NoArgs,
   803  		ValidArgsFunction: NoFileCompletions,
   804  		RunE: func(cmd *Command, args []string) error {
   805  			return cmd.Root().GenFishCompletion(out, !noDesc)
   806  		},
   807  	}
   808  	if haveNoDescFlag {
   809  		fish.Flags().BoolVar(&noDesc, compCmdNoDescFlagName, compCmdNoDescFlagDefault, compCmdNoDescFlagDesc)
   810  	}
   811  
   812  	powershell := &Command{
   813  		Use:   "powershell",
   814  		Short: fmt.Sprintf(shortDesc, "powershell"),
   815  		Long: fmt.Sprintf(`Generate the autocompletion script for powershell.
   816  
   817  To load completions in your current shell session:
   818  
   819  	%[1]s completion powershell | Out-String | Invoke-Expression
   820  
   821  To load completions for every new session, add the output of the above command
   822  to your powershell profile.
   823  `, c.Root().Name()),
   824  		Args:              NoArgs,
   825  		ValidArgsFunction: NoFileCompletions,
   826  		RunE: func(cmd *Command, args []string) error {
   827  			if noDesc {
   828  				return cmd.Root().GenPowerShellCompletion(out)
   829  			}
   830  			return cmd.Root().GenPowerShellCompletionWithDesc(out)
   831  
   832  		},
   833  	}
   834  	if haveNoDescFlag {
   835  		powershell.Flags().BoolVar(&noDesc, compCmdNoDescFlagName, compCmdNoDescFlagDefault, compCmdNoDescFlagDesc)
   836  	}
   837  
   838  	completionCmd.AddCommand(bash, zsh, fish, powershell)
   839  }
   840  
   841  func findFlag(cmd *Command, name string) *pflag.Flag {
   842  	flagSet := cmd.Flags()
   843  	if len(name) == 1 {
   844  		// First convert the short flag into a long flag
   845  		// as the cmd.Flag() search only accepts long flags
   846  		if short := flagSet.ShorthandLookup(name); short != nil {
   847  			name = short.Name
   848  		} else {
   849  			set := cmd.InheritedFlags()
   850  			if short = set.ShorthandLookup(name); short != nil {
   851  				name = short.Name
   852  			} else {
   853  				return nil
   854  			}
   855  		}
   856  	}
   857  	return cmd.Flag(name)
   858  }
   859  
   860  // CompDebug prints the specified string to the same file as where the
   861  // completion script prints its logs.
   862  // Note that completion printouts should never be on stdout as they would
   863  // be wrongly interpreted as actual completion choices by the completion script.
   864  func CompDebug(msg string, printToStdErr bool) {
   865  	msg = fmt.Sprintf("[Debug] %s", msg)
   866  
   867  	// Such logs are only printed when the user has set the environment
   868  	// variable BASH_COMP_DEBUG_FILE to the path of some file to be used.
   869  	if path := os.Getenv("BASH_COMP_DEBUG_FILE"); path != "" {
   870  		f, err := os.OpenFile(path,
   871  			os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
   872  		if err == nil {
   873  			defer f.Close()
   874  			WriteStringAndCheck(f, msg)
   875  		}
   876  	}
   877  
   878  	if printToStdErr {
   879  		// Must print to stderr for this not to be read by the completion script.
   880  		fmt.Fprint(os.Stderr, msg)
   881  	}
   882  }
   883  
   884  // CompDebugln prints the specified string with a newline at the end
   885  // to the same file as where the completion script prints its logs.
   886  // Such logs are only printed when the user has set the environment
   887  // variable BASH_COMP_DEBUG_FILE to the path of some file to be used.
   888  func CompDebugln(msg string, printToStdErr bool) {
   889  	CompDebug(fmt.Sprintf("%s\n", msg), printToStdErr)
   890  }
   891  
   892  // CompError prints the specified completion message to stderr.
   893  func CompError(msg string) {
   894  	msg = fmt.Sprintf("[Error] %s", msg)
   895  	CompDebug(msg, true)
   896  }
   897  
   898  // CompErrorln prints the specified completion message to stderr with a newline at the end.
   899  func CompErrorln(msg string) {
   900  	CompError(fmt.Sprintf("%s\n", msg))
   901  }
   902  

View as plain text