From 7dfd3d11f6c358c332fe57871038c3cea8d910a5 Mon Sep 17 00:00:00 2001 From: Marc Khouzam Date: Fri, 21 Feb 2020 19:24:41 -0500 Subject: [PATCH] feat(comp): Adapt __complete for basic completions The __complete command is extended to provide completions all cases: - custom completions (as it already did) - command names - flag names This will allow to use the __complete command to provide completions for shells that don't have Cobra support. It could even be used to improve bash completion support eventually. Signed-off-by: Marc Khouzam --- internal/completion/complete.go | 214 ++++++++++++++++++++++---------- 1 file changed, 145 insertions(+), 69 deletions(-) diff --git a/internal/completion/complete.go b/internal/completion/complete.go index eaea4e914..16ee032b5 100644 --- a/internal/completion/complete.go +++ b/internal/completion/complete.go @@ -35,8 +35,8 @@ import ( // This should ultimately be pushed down into Cobra. // ================================================================================== -// CompRequestCmd Hidden command to request completion results from the program. -// Used by the shell completion script. +// CompRequestCmd is the name of the hidden command that is used to request +// completion results from helm. It is used by the shell completion script. const CompRequestCmd = "__complete" // Global map allowing to find completion functions for commands or flags. @@ -73,7 +73,7 @@ __helm_custom_func() __helm_debug "${FUNCNAME[0]}: c is $c, words[@] is ${words[@]}, #words[@] is ${#words[@]}" __helm_debug "${FUNCNAME[0]}: cur is ${cur}, cword is ${cword}, words is ${words}" - local out requestComp lastParam lastChar + local out requestComp lastParam lastChar comp directive requestComp="${words[0]} %[1]s ${words[@]:1}" lastParam=${words[$((${#words[@]}-1))]} @@ -173,7 +173,7 @@ func (d BashCompDirective) string() string { return strings.Join(directives, ", ") } -// NewCompleteCmd add a special hidden command that an be used to request completions +// NewCompleteCmd adds a special hidden command that an be used to request completions func NewCompleteCmd(settings *cli.EnvSettings, out io.Writer) *cobra.Command { debug = settings.Debug return &cobra.Command{ @@ -183,69 +183,17 @@ func NewCompleteCmd(settings *cli.EnvSettings, out io.Writer) *cobra.Command { DisableFlagParsing: true, Args: require.MinimumNArgs(1), Short: "Request shell completion choices for the specified command-line", - Long: fmt.Sprintf("%s is a special command that is used by the shell completion logic\n%s", - CompRequestCmd, "to request 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.", CompRequestCmd), Run: func(cmd *cobra.Command, args []string) { - CompDebugln(fmt.Sprintf("%s was called with args %v", cmd.Name(), args)) - - // The last argument, which is not complete, 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 := cmd.Root().Find(trimmedArgs) + completions, directive, err := getCompletions(cmd.Root(), args) if err != nil { - // Unable to find the real command. E.g., helm invalidCmd - CompDebugln(fmt.Sprintf("Unable to find a command for arguments: %v", trimmedArgs)) - return - } - - CompDebugln(fmt.Sprintf("Found final command '%s', with finalArgs %v", finalCmd.Name(), finalArgs)) - - var flag *pflag.Flag - if !finalCmd.DisableFlagParsing { - // We only do flag completion if we are allowed to parse flags - // This is important for helm plugins which need to do their own flag completion. - flag, finalArgs, toComplete, err = checkIfFlagCompletion(finalCmd, finalArgs, toComplete) - if err != nil { - // Error while attempting to parse flags - CompErrorln(err.Error()) - return - } - } - - // Parse the flags and extract the arguments to prepare for calling the completion function - if err = finalCmd.ParseFlags(finalArgs); err != nil { - CompErrorln(fmt.Sprintf("Error while parsing flags from args %v: %s", finalArgs, err.Error())) - return + 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 } - // We only remove the flags from the arguments if DisableFlagParsing is not set. - // This is important for helm plugins, which need to receive all flags. - // The plugin completion code will do its own flag parsing. - if !finalCmd.DisableFlagParsing { - finalArgs = finalCmd.Flags().Args() - CompDebugln(fmt.Sprintf("Args without flags are '%v' with length %d", finalArgs, len(finalArgs))) - } - - // Find completion function for the flag or command - var key interface{} - var keyStr string - if flag != nil { - key = flag - keyStr = flag.Name - } else { - key = finalCmd - keyStr = finalCmd.Name() - } - completionFn, ok := validArgsFunctions[key] - if !ok { - CompErrorln(fmt.Sprintf("Dynamic completion not supported/needed for flag or command: %s", keyStr)) - return - } - - CompDebugln(fmt.Sprintf("Calling completion method for subcommand '%s' with args '%v' and toComplete '%s'", finalCmd.Name(), finalArgs, toComplete)) - completions, directive := completionFn(finalCmd, finalArgs, toComplete) for _, comp := range completions { // Print each possible completion to stdout for the completion script to consume. fmt.Fprintln(out, comp) @@ -255,23 +203,149 @@ func NewCompleteCmd(settings *cli.EnvSettings, out io.Writer) *cobra.Command { directive = BashCompDirectiveDefault } - // 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 : - // The completion script expects :directive + // 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 should be ignored from the completion script. + // Output from stderr should be ignored by the completion script. fmt.Fprintf(os.Stderr, "Completion ended with directive: %s\n", directive.string()) }, } } +func getCompletions(rootCmd *cobra.Command, args []string) ([]string, BashCompDirective, 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 := rootCmd.Find(trimmedArgs) + if err != nil { + // Unable to find the real command. E.g., helm invalidCmd + return completions, BashCompDirectiveDefault, fmt.Errorf("Unable to find a command for arguments: %v", trimmedArgs) + } + + if isFlag(toComplete) && !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 := BashCompDirectiveDefault + if len(completions) > 0 { + if strings.HasSuffix(completions[0], "=") { + directive = BashCompDirectiveNoSpace + } + } + return 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 helm plugins which need to do their own flag completion. + flag, finalArgs, toComplete, err = checkIfFlagCompletion(finalCmd, finalArgs, toComplete) + if err != nil { + // Error while attempting to parse flags + return completions, BashCompDirectiveDefault, err + } + } + + if flag == nil { + // Complete subcommand names + for _, subCmd := range finalCmd.Commands() { + if subCmd.IsAvailableCommand() && strings.HasPrefix(subCmd.Name(), toComplete) { + completions = append(completions, subCmd.Name()) + } + } + + // 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) + } + } + + // Always let the logic continue to add any ValidArgsFunction completions, + // even if we already found other completions already. + // This is for commands that have subcommands and/or validArgs 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 completions, BashCompDirectiveDefault, 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 helm plugins, which need to receive all flags. + // The plugin completion code will do its own flag parsing. + if !finalCmd.DisableFlagParsing { + finalArgs = finalCmd.Flags().Args() + } + + // Find completion function for the flag or command + var key interface{} + var nameStr string + if flag != nil { + key = flag + nameStr = flag.Name + } else { + key = finalCmd + nameStr = finalCmd.CommandPath() + } + completionFn, ok := validArgsFunctions[key] + if !ok { + return completions, BashCompDirectiveDefault, fmt.Errorf("Go custom completion not supported/needed for flag or command: %s", nameStr) + } + + comps, directive := completionFn(finalCmd, finalArgs, toComplete) + completions = append(completions, comps...) + return completions, directive, nil +} + +func getFlagNameCompletions(flag *pflag.Flag, toComplete string) []string { + if nonCompletableFlag(flag) { + return []string{} + } + + var completions []string + comp := "--" + flag.Name + if strings.HasPrefix(comp, toComplete) { + // Flag without the = + completions = append(completions, comp) + + if len(flag.NoOptDefVal) == 0 { + // Flag requires a value, so it can be suffixed with = + comp += "=" + completions = append(completions, comp) + } + } + + comp = "-" + flag.Shorthand + if len(flag.Shorthand) > 0 && strings.HasPrefix(comp, toComplete) { + completions = append(completions, comp) + } + return completions +} + func isFlag(arg string) bool { return len(arg) > 0 && arg[0] == '-' } +func nonCompletableFlag(flag *pflag.Flag) bool { + return flag.Hidden || len(flag.Deprecated) > 0 +} + func checkIfFlagCompletion(finalCmd *cobra.Command, args []string, lastArg string) (*pflag.Flag, []string, string, error) { var flagName string trimmedArgs := args @@ -290,7 +364,9 @@ func checkIfFlagCompletion(finalCmd *cobra.Command, args []string, lastArg strin if len(args) > 0 { prevArg := args[len(args)-1] if isFlag(prevArg) { - // If the flag contains an = it means it has already been fully processed + // 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, "-")