385 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Go
		
	
	
			
		
		
	
	
			385 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Go
		
	
	
| package cobra
 | |
| 
 | |
| import (
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"os"
 | |
| 	"strings"
 | |
| 
 | |
| 	"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.
 | |
| var flagCompletionFunctions = map[*pflag.Flag]func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective){}
 | |
| 
 | |
| // ShellCompDirective is a bit map representing the different behaviors the shell
 | |
| // can be instructed to have once completions have been provided.
 | |
| type ShellCompDirective int
 | |
| 
 | |
| 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.
 | |
| 	// This currently does not work for zsh or bash < 4
 | |
| 	ShellCompDirectiveNoFileComp
 | |
| 
 | |
| 	// ShellCompDirectiveDefault indicates to let the shell perform its default
 | |
| 	// behavior after completions have been provided.
 | |
| 	ShellCompDirectiveDefault ShellCompDirective = 0
 | |
| )
 | |
| 
 | |
| // RegisterFlagCompletionFunc should be called to register a function to provide completion for a flag.
 | |
| func (c *Command) RegisterFlagCompletionFunc(flagName string, f func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective)) error {
 | |
| 	flag := c.Flag(flagName)
 | |
| 	if flag == nil {
 | |
| 		return fmt.Errorf("RegisterFlagCompletionFunc: flag '%s' does not exist", flagName)
 | |
| 	}
 | |
| 	if _, exists := flagCompletionFunctions[flag]; exists {
 | |
| 		return fmt.Errorf("RegisterFlagCompletionFunc: flag '%s' already registered", flagName)
 | |
| 	}
 | |
| 	flagCompletionFunctions[flag] = f
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // Returns a string listing the different directive enabled in the specified parameter
 | |
| func (d 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 len(directives) == 0 {
 | |
| 		directives = append(directives, "ShellCompDirectiveDefault")
 | |
| 	}
 | |
| 
 | |
| 	if d > ShellCompDirectiveError+ShellCompDirectiveNoSpace+ShellCompDirectiveNoFileComp {
 | |
| 		return fmt.Sprintf("ERROR: unexpected ShellCompDirective value: %d", d)
 | |
| 	}
 | |
| 	return strings.Join(directives, ", ")
 | |
| }
 | |
| 
 | |
| // Adds a special hidden command that can be used to request custom completions.
 | |
| func (c *Command) initCompleteCmd(args []string) {
 | |
| 	completeCmd := &Command{
 | |
| 		Use:                   fmt.Sprintf("%s [command-line]", 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)
 | |
| 			for _, comp := range completions {
 | |
| 				if noDescriptions {
 | |
| 					// Remove any description that may be included following a tab character.
 | |
| 					comp = strings.Split(comp, "\t")[0]
 | |
| 				}
 | |
| 				// Print each possible completion to stdout for the completion script to consume.
 | |
| 				fmt.Fprintln(finalCmd.OutOrStdout(), comp)
 | |
| 			}
 | |
| 
 | |
| 			if directive > ShellCompDirectiveError+ShellCompDirectiveNoSpace+ShellCompDirectiveNoFileComp {
 | |
| 				directive = ShellCompDirectiveDefault
 | |
| 			}
 | |
| 
 | |
| 			// As the last printout, print the completion directive for the completion script to parse.
 | |
| 			// The directive integer must be that last character following a single colon (:).
 | |
| 			// The completion script expects :<directive>
 | |
| 			fmt.Fprintf(finalCmd.OutOrStdout(), ":%d\n", directive)
 | |
| 
 | |
| 			// Print some helpful info to stderr for the user to understand.
 | |
| 			// Output from stderr must be ignored by the completion script.
 | |
| 			fmt.Fprintf(finalCmd.ErrOrStderr(), "Completion ended with directive: %s\n", directive.string())
 | |
| 		},
 | |
| 	}
 | |
| 	c.AddCommand(completeCmd)
 | |
| 	subCmd, _, err := c.Find(args)
 | |
| 	if err != nil || subCmd.Name() != 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) {
 | |
| 	var completions []string
 | |
| 
 | |
| 	// The last argument, which is not completely typed by the user,
 | |
| 	// should not be part of the list of arguments
 | |
| 	toComplete := args[len(args)-1]
 | |
| 	trimmedArgs := args[:len(args)-1]
 | |
| 
 | |
| 	// Find the real command for which completion must be performed
 | |
| 	finalCmd, finalArgs, err := c.Root().Find(trimmedArgs)
 | |
| 	if err != nil {
 | |
| 		// Unable to find the real command. E.g., <program> someInvalidCmd <TAB>
 | |
| 		return c, completions, ShellCompDirectiveDefault, fmt.Errorf("Unable to find a command for arguments: %v", trimmedArgs)
 | |
| 	}
 | |
| 
 | |
| 	// 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 to be complete
 | |
| 	if len(toComplete) > 0 && toComplete[0] == '-' && !strings.Contains(toComplete, "=") {
 | |
| 		// We are completing a flag name
 | |
| 		finalCmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) {
 | |
| 			completions = append(completions, getFlagNameCompletions(flag, toComplete)...)
 | |
| 		})
 | |
| 		finalCmd.InheritedFlags().VisitAll(func(flag *pflag.Flag) {
 | |
| 			completions = append(completions, getFlagNameCompletions(flag, toComplete)...)
 | |
| 		})
 | |
| 
 | |
| 		directive := ShellCompDirectiveDefault
 | |
| 		if len(completions) > 0 {
 | |
| 			if strings.HasSuffix(completions[0], "=") {
 | |
| 				directive = ShellCompDirectiveNoSpace
 | |
| 			}
 | |
| 		}
 | |
| 		return finalCmd, completions, directive, nil
 | |
| 	}
 | |
| 
 | |
| 	var flag *pflag.Flag
 | |
| 	if !finalCmd.DisableFlagParsing {
 | |
| 		// We only do flag completion if we are allowed to parse flags
 | |
| 		// This is important for commands which have requested to do their own flag completion.
 | |
| 		flag, finalArgs, toComplete, err = checkIfFlagCompletion(finalCmd, finalArgs, toComplete)
 | |
| 		if err != nil {
 | |
| 			// Error while attempting to parse flags
 | |
| 			return finalCmd, completions, ShellCompDirectiveDefault, err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if flag == nil {
 | |
| 		// Complete subcommand names
 | |
| 		for _, subCmd := range finalCmd.Commands() {
 | |
| 			if subCmd.IsAvailableCommand() && strings.HasPrefix(subCmd.Name(), toComplete) {
 | |
| 				completions = append(completions, fmt.Sprintf("%s\t%s", subCmd.Name(), subCmd.Short))
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if len(finalCmd.ValidArgs) > 0 {
 | |
| 			// Always complete ValidArgs, even if we are completing a subcommand name.
 | |
| 			// This is for commands that have both subcommands and ValidArgs.
 | |
| 			for _, validArg := range finalCmd.ValidArgs {
 | |
| 				if strings.HasPrefix(validArg, toComplete) {
 | |
| 					completions = append(completions, validArg)
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			// 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, ShellCompDirectiveNoFileComp, nil
 | |
| 		}
 | |
| 
 | |
| 		// Always 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.
 | |
| 	}
 | |
| 
 | |
| 	// Parse the flags and extract the arguments to prepare for calling the completion function
 | |
| 	if err = finalCmd.ParseFlags(finalArgs); err != nil {
 | |
| 		return finalCmd, completions, ShellCompDirectiveDefault, fmt.Errorf("Error while parsing flags from args %v: %s", finalArgs, err.Error())
 | |
| 	}
 | |
| 
 | |
| 	// We only remove the flags from the arguments if DisableFlagParsing is not set.
 | |
| 	// This is important for commands which have requested to do their own flag completion.
 | |
| 	if !finalCmd.DisableFlagParsing {
 | |
| 		finalArgs = finalCmd.Flags().Args()
 | |
| 	}
 | |
| 
 | |
| 	// Find the completion function for the flag or command
 | |
| 	var completionFn func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective)
 | |
| 	if flag != nil {
 | |
| 		completionFn = flagCompletionFunctions[flag]
 | |
| 	} else {
 | |
| 		completionFn = finalCmd.ValidArgsFunction
 | |
| 	}
 | |
| 	if completionFn == nil {
 | |
| 		// Go custom completion not supported/needed for this flag or command
 | |
| 		return finalCmd, completions, ShellCompDirectiveDefault, nil
 | |
| 	}
 | |
| 
 | |
| 	// Call the registered completion function to get the completions
 | |
| 	comps, directive := completionFn(finalCmd, finalArgs, toComplete)
 | |
| 	completions = append(completions, comps...)
 | |
| 	return finalCmd, completions, directive, nil
 | |
| }
 | |
| 
 | |
| func 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))
 | |
| 
 | |
| 		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 checkIfFlagCompletion(finalCmd *Command, args []string, lastArg string) (*pflag.Flag, []string, string, error) {
 | |
| 	var flagName string
 | |
| 	trimmedArgs := args
 | |
| 	flagWithEqual := false
 | |
| 	if isFlagArg(lastArg) {
 | |
| 		if index := strings.Index(lastArg, "="); index >= 0 {
 | |
| 			flagName = strings.TrimLeft(lastArg[:index], "-")
 | |
| 			lastArg = lastArg[index+1:]
 | |
| 			flagWithEqual = true
 | |
| 		} else {
 | |
| 			return nil, nil, "", errors.New("Unexpected completion request for flag")
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if len(flagName) == 0 {
 | |
| 		if len(args) > 0 {
 | |
| 			prevArg := args[len(args)-1]
 | |
| 			if isFlagArg(prevArg) {
 | |
| 				// Only consider the case where the flag does not contain an =.
 | |
| 				// If the flag contains an = it means it has already been fully processed,
 | |
| 				// so we don't need to deal with it here.
 | |
| 				if index := strings.Index(prevArg, "="); index < 0 {
 | |
| 					flagName = strings.TrimLeft(prevArg, "-")
 | |
| 
 | |
| 					// Remove the uncompleted flag or else there could be an error created
 | |
| 					// for an invalid value for that flag
 | |
| 					trimmedArgs = args[:len(args)-1]
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if len(flagName) == 0 {
 | |
| 		// Not doing flag completion
 | |
| 		return nil, trimmedArgs, lastArg, nil
 | |
| 	}
 | |
| 
 | |
| 	flag := findFlag(finalCmd, flagName)
 | |
| 	if flag == nil {
 | |
| 		// Flag not supported by this command, nothing to complete
 | |
| 		err := fmt.Errorf("Subcommand '%s' does not support flag '%s'", finalCmd.Name(), flagName)
 | |
| 		return nil, nil, "", err
 | |
| 	}
 | |
| 
 | |
| 	if !flagWithEqual {
 | |
| 		if len(flag.NoOptDefVal) != 0 {
 | |
| 			// We had assumed dealing with a two-word flag but the flag is a boolean flag.
 | |
| 			// In that case, there is no value following it, so we are not really doing flag completion.
 | |
| 			// Reset everything to do noun completion.
 | |
| 			trimmedArgs = args
 | |
| 			flag = nil
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return flag, trimmedArgs, lastArg, nil
 | |
| }
 | |
| 
 | |
| func findFlag(cmd *Command, name string) *pflag.Flag {
 | |
| 	flagSet := cmd.Flags()
 | |
| 	if len(name) == 1 {
 | |
| 		// First convert the short flag into a long flag
 | |
| 		// as the cmd.Flag() search only accepts long flags
 | |
| 		if short := flagSet.ShorthandLookup(name); short != nil {
 | |
| 			name = short.Name
 | |
| 		} else {
 | |
| 			set := cmd.InheritedFlags()
 | |
| 			if short = set.ShorthandLookup(name); short != nil {
 | |
| 				name = short.Name
 | |
| 			} else {
 | |
| 				return nil
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return cmd.Flag(name)
 | |
| }
 | |
| 
 | |
| // CompDebug prints the specified string to the same file as where the
 | |
| // completion script prints its logs.
 | |
| // Note that completion printouts should never be on stdout as they would
 | |
| // be wrongly interpreted as actual completion choices by the completion script.
 | |
| func CompDebug(msg string, printToStdErr bool) {
 | |
| 	msg = fmt.Sprintf("[Debug] %s", msg)
 | |
| 
 | |
| 	// Such logs are only printed when the user has set the environment
 | |
| 	// variable BASH_COMP_DEBUG_FILE to the path of some file to be used.
 | |
| 	if path := os.Getenv("BASH_COMP_DEBUG_FILE"); path != "" {
 | |
| 		f, err := os.OpenFile(path,
 | |
| 			os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
 | |
| 		if err == nil {
 | |
| 			defer f.Close()
 | |
| 			f.WriteString(msg)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if printToStdErr {
 | |
| 		// Must print to stderr for this not to be read by the completion script.
 | |
| 		fmt.Fprintf(os.Stderr, msg)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // CompDebugln prints the specified string with a newline at the end
 | |
| // to the same file as where the completion script prints its logs.
 | |
| // Such logs are only printed when the user has set the environment
 | |
| // variable BASH_COMP_DEBUG_FILE to the path of some file to be used.
 | |
| func CompDebugln(msg string, printToStdErr bool) {
 | |
| 	CompDebug(fmt.Sprintf("%s\n", msg), printToStdErr)
 | |
| }
 | |
| 
 | |
| // CompError prints the specified completion message to stderr.
 | |
| func CompError(msg string) {
 | |
| 	msg = fmt.Sprintf("[Error] %s", msg)
 | |
| 	CompDebug(msg, true)
 | |
| }
 | |
| 
 | |
| // CompErrorln prints the specified completion message to stderr with a newline at the end.
 | |
| func CompErrorln(msg string) {
 | |
| 	CompError(fmt.Sprintf("%s\n", msg))
 | |
| }
 |