github.com/codefresh-io/kcfi@v0.0.0-20230301195427-c1578715cc46/pkg/helm-internal/completion/complete.go (about)

     1  /*
     2  Copyright The Helm Authors.
     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  
    16  package completion
    17  
    18  import (
    19  	"errors"
    20  	"fmt"
    21  	"io"
    22  	"log"
    23  	"os"
    24  	"strings"
    25  
    26  	"github.com/spf13/cobra"
    27  	"github.com/spf13/pflag"
    28  
    29  	"helm.sh/helm/v3/cmd/helm/require"
    30  	"helm.sh/helm/v3/pkg/cli"
    31  )
    32  
    33  // ==================================================================================
    34  // The below code supports dynamic shell completion in Go.
    35  // This should ultimately be pushed down into Cobra.
    36  // ==================================================================================
    37  
    38  // CompRequestCmd Hidden command to request completion results from the program.
    39  // Used by the shell completion script.
    40  const CompRequestCmd = "__complete"
    41  
    42  // Global map allowing to find completion functions for commands or flags.
    43  var validArgsFunctions = map[interface{}]func(cmd *cobra.Command, args []string, toComplete string) ([]string, BashCompDirective){}
    44  
    45  // BashCompDirective is a bit map representing the different behaviors the shell
    46  // can be instructed to have once completions have been provided.
    47  type BashCompDirective int
    48  
    49  const (
    50  	// BashCompDirectiveError indicates an error occurred and completions should be ignored.
    51  	BashCompDirectiveError BashCompDirective = 1 << iota
    52  
    53  	// BashCompDirectiveNoSpace indicates that the shell should not add a space
    54  	// after the completion even if there is a single completion provided.
    55  	BashCompDirectiveNoSpace
    56  
    57  	// BashCompDirectiveNoFileComp indicates that the shell should not provide
    58  	// file completion even when no completion is provided.
    59  	// This currently does not work for zsh or bash < 4
    60  	BashCompDirectiveNoFileComp
    61  
    62  	// BashCompDirectiveDefault indicates to let the shell perform its default
    63  	// behavior after completions have been provided.
    64  	BashCompDirectiveDefault BashCompDirective = 0
    65  )
    66  
    67  // GetBashCustomFunction returns the bash code to handle custom go completion
    68  // This should eventually be provided by Cobra
    69  func GetBashCustomFunction() string {
    70  	return fmt.Sprintf(`
    71  __helm_custom_func()
    72  {
    73      __helm_debug "${FUNCNAME[0]}: c is $c, words[@] is ${words[@]}, #words[@] is ${#words[@]}"
    74      __helm_debug "${FUNCNAME[0]}: cur is ${cur}, cword is ${cword}, words is ${words}"
    75  
    76      local out requestComp lastParam lastChar
    77      requestComp="${words[0]} %[1]s ${words[@]:1}"
    78  
    79      lastParam=${words[$((${#words[@]}-1))]}
    80      lastChar=${lastParam:$((${#lastParam}-1)):1}
    81      __helm_debug "${FUNCNAME[0]}: lastParam ${lastParam}, lastChar ${lastChar}"
    82  
    83      if [ -z "${cur}" ] && [ "${lastChar}" != "=" ]; then
    84          # If the last parameter is complete (there is a space following it)
    85          # We add an extra empty parameter so we can indicate this to the go method.
    86          __helm_debug "${FUNCNAME[0]}: Adding extra empty parameter"
    87          requestComp="${requestComp} \"\""
    88      fi
    89  
    90      __helm_debug "${FUNCNAME[0]}: calling ${requestComp}"
    91      # Use eval to handle any environment variables and such
    92      out=$(eval ${requestComp} 2>/dev/null)
    93  
    94      # Extract the directive int at the very end of the output following a :
    95      directive=${out##*:}
    96      # Remove the directive
    97      out=${out%%:*}
    98      if [ "${directive}" = "${out}" ]; then
    99          # There is not directive specified
   100          directive=0
   101      fi
   102      __helm_debug "${FUNCNAME[0]}: the completion directive is: ${directive}"
   103      __helm_debug "${FUNCNAME[0]}: the completions are: ${out[*]}"
   104  
   105      if [ $((${directive} & %[2]d)) -ne 0 ]; then
   106          __helm_debug "${FUNCNAME[0]}: received error, completion failed"
   107      else
   108          if [ $((${directive} & %[3]d)) -ne 0 ]; then
   109              if [[ $(type -t compopt) = "builtin" ]]; then
   110                  __helm_debug "${FUNCNAME[0]}: activating no space"
   111                  compopt -o nospace
   112              fi
   113          fi
   114          if [ $((${directive} & %[4]d)) -ne 0 ]; then
   115              if [[ $(type -t compopt) = "builtin" ]]; then
   116                  __helm_debug "${FUNCNAME[0]}: activating no file completion"
   117                  compopt +o default
   118              fi
   119          fi
   120  
   121          while IFS='' read -r comp; do
   122              COMPREPLY+=("$comp")
   123          done < <(compgen -W "${out[*]}" -- "$cur")
   124      fi
   125  }
   126  `, CompRequestCmd, BashCompDirectiveError, BashCompDirectiveNoSpace, BashCompDirectiveNoFileComp)
   127  }
   128  
   129  // RegisterValidArgsFunc should be called to register a function to provide argument completion for a command
   130  func RegisterValidArgsFunc(cmd *cobra.Command, f func(cmd *cobra.Command, args []string, toComplete string) ([]string, BashCompDirective)) {
   131  	if _, exists := validArgsFunctions[cmd]; exists {
   132  		log.Fatal(fmt.Sprintf("RegisterValidArgsFunc: command '%s' already registered", cmd.Name()))
   133  	}
   134  	validArgsFunctions[cmd] = f
   135  }
   136  
   137  // RegisterFlagCompletionFunc should be called to register a function to provide completion for a flag
   138  func RegisterFlagCompletionFunc(flag *pflag.Flag, f func(cmd *cobra.Command, args []string, toComplete string) ([]string, BashCompDirective)) {
   139  	if _, exists := validArgsFunctions[flag]; exists {
   140  		log.Fatal(fmt.Sprintf("RegisterFlagCompletionFunc: flag '%s' already registered", flag.Name))
   141  	}
   142  	validArgsFunctions[flag] = f
   143  
   144  	// Make sure the completion script call the __helm_custom_func for the registered flag.
   145  	// This is essential to make the = form work. E.g., helm -n=<TAB> or helm status --output=<TAB>
   146  	if flag.Annotations == nil {
   147  		flag.Annotations = map[string][]string{}
   148  	}
   149  	flag.Annotations[cobra.BashCompCustom] = []string{"__helm_custom_func"}
   150  }
   151  
   152  var debug = true
   153  
   154  // Returns a string listing the different directive enabled in the specified parameter
   155  func (d BashCompDirective) string() string {
   156  	var directives []string
   157  	if d&BashCompDirectiveError != 0 {
   158  		directives = append(directives, "BashCompDirectiveError")
   159  	}
   160  	if d&BashCompDirectiveNoSpace != 0 {
   161  		directives = append(directives, "BashCompDirectiveNoSpace")
   162  	}
   163  	if d&BashCompDirectiveNoFileComp != 0 {
   164  		directives = append(directives, "BashCompDirectiveNoFileComp")
   165  	}
   166  	if len(directives) == 0 {
   167  		directives = append(directives, "BashCompDirectiveDefault")
   168  	}
   169  
   170  	if d > BashCompDirectiveError+BashCompDirectiveNoSpace+BashCompDirectiveNoFileComp {
   171  		return fmt.Sprintf("ERROR: unexpected BashCompDirective value: %d", d)
   172  	}
   173  	return strings.Join(directives, ", ")
   174  }
   175  
   176  // NewCompleteCmd add a special hidden command that an be used to request completions
   177  func NewCompleteCmd(settings *cli.EnvSettings, out io.Writer) *cobra.Command {
   178  	debug = settings.Debug
   179  	return &cobra.Command{
   180  		Use:                   fmt.Sprintf("%s [command-line]", CompRequestCmd),
   181  		DisableFlagsInUseLine: true,
   182  		Hidden:                true,
   183  		DisableFlagParsing:    true,
   184  		Args:                  require.MinimumNArgs(1),
   185  		Short:                 "Request shell completion choices for the specified command-line",
   186  		Long: fmt.Sprintf("%s is a special command that is used by the shell completion logic\n%s",
   187  			CompRequestCmd, "to request completion choices for the specified command-line."),
   188  		Run: func(cmd *cobra.Command, args []string) {
   189  			CompDebugln(fmt.Sprintf("%s was called with args %v", cmd.Name(), args))
   190  
   191  			// The last argument, which is not complete, should not be part of the list of arguments
   192  			toComplete := args[len(args)-1]
   193  			trimmedArgs := args[:len(args)-1]
   194  
   195  			// Find the real command for which completion must be performed
   196  			finalCmd, finalArgs, err := cmd.Root().Find(trimmedArgs)
   197  			if err != nil {
   198  				// Unable to find the real command. E.g., helm invalidCmd <TAB>
   199  				CompDebugln(fmt.Sprintf("Unable to find a command for arguments: %v", trimmedArgs))
   200  				return
   201  			}
   202  
   203  			CompDebugln(fmt.Sprintf("Found final command '%s', with finalArgs %v", finalCmd.Name(), finalArgs))
   204  
   205  			var flag *pflag.Flag
   206  			if !finalCmd.DisableFlagParsing {
   207  				// We only do flag completion if we are allowed to parse flags
   208  				// This is important for helm plugins which need to do their own flag completion.
   209  				flag, finalArgs, toComplete, err = checkIfFlagCompletion(finalCmd, finalArgs, toComplete)
   210  				if err != nil {
   211  					// Error while attempting to parse flags
   212  					CompErrorln(err.Error())
   213  					return
   214  				}
   215  			}
   216  
   217  			// Parse the flags and extract the arguments to prepare for calling the completion function
   218  			if err = finalCmd.ParseFlags(finalArgs); err != nil {
   219  				CompErrorln(fmt.Sprintf("Error while parsing flags from args %v: %s", finalArgs, err.Error()))
   220  				return
   221  			}
   222  
   223  			// We only remove the flags from the arguments if DisableFlagParsing is not set.
   224  			// This is important for helm plugins, which need to receive all flags.
   225  			// The plugin completion code will do its own flag parsing.
   226  			if !finalCmd.DisableFlagParsing {
   227  				finalArgs = finalCmd.Flags().Args()
   228  				CompDebugln(fmt.Sprintf("Args without flags are '%v' with length %d", finalArgs, len(finalArgs)))
   229  			}
   230  
   231  			// Find completion function for the flag or command
   232  			var key interface{}
   233  			var keyStr string
   234  			if flag != nil {
   235  				key = flag
   236  				keyStr = flag.Name
   237  			} else {
   238  				key = finalCmd
   239  				keyStr = finalCmd.Name()
   240  			}
   241  			completionFn, ok := validArgsFunctions[key]
   242  			if !ok {
   243  				CompErrorln(fmt.Sprintf("Dynamic completion not supported/needed for flag or command: %s", keyStr))
   244  				return
   245  			}
   246  
   247  			CompDebugln(fmt.Sprintf("Calling completion method for subcommand '%s' with args '%v' and toComplete '%s'", finalCmd.Name(), finalArgs, toComplete))
   248  			completions, directive := completionFn(finalCmd, finalArgs, toComplete)
   249  			for _, comp := range completions {
   250  				// Print each possible completion to stdout for the completion script to consume.
   251  				fmt.Fprintln(out, comp)
   252  			}
   253  
   254  			if directive > BashCompDirectiveError+BashCompDirectiveNoSpace+BashCompDirectiveNoFileComp {
   255  				directive = BashCompDirectiveDefault
   256  			}
   257  
   258  			// As the last printout, print the completion directive for the
   259  			// completion script to parse.
   260  			// The directive integer must be that last character following a single :
   261  			// The completion script expects :directive
   262  			fmt.Fprintf(out, ":%d\n", directive)
   263  
   264  			// Print some helpful info to stderr for the user to understand.
   265  			// Output from stderr should be ignored from the completion script.
   266  			fmt.Fprintf(os.Stderr, "Completion ended with directive: %s\n", directive.string())
   267  		},
   268  	}
   269  }
   270  
   271  func isFlag(arg string) bool {
   272  	return len(arg) > 0 && arg[0] == '-'
   273  }
   274  
   275  func checkIfFlagCompletion(finalCmd *cobra.Command, args []string, lastArg string) (*pflag.Flag, []string, string, error) {
   276  	var flagName string
   277  	trimmedArgs := args
   278  	flagWithEqual := false
   279  	if isFlag(lastArg) {
   280  		if index := strings.Index(lastArg, "="); index >= 0 {
   281  			flagName = strings.TrimLeft(lastArg[:index], "-")
   282  			lastArg = lastArg[index+1:]
   283  			flagWithEqual = true
   284  		} else {
   285  			return nil, nil, "", errors.New("Unexpected completion request for flag")
   286  		}
   287  	}
   288  
   289  	if len(flagName) == 0 {
   290  		if len(args) > 0 {
   291  			prevArg := args[len(args)-1]
   292  			if isFlag(prevArg) {
   293  				// If the flag contains an = it means it has already been fully processed
   294  				if index := strings.Index(prevArg, "="); index < 0 {
   295  					flagName = strings.TrimLeft(prevArg, "-")
   296  
   297  					// Remove the uncompleted flag or else Cobra could complain about
   298  					// an invalid value for that flag e.g., helm status --output j<TAB>
   299  					trimmedArgs = args[:len(args)-1]
   300  				}
   301  			}
   302  		}
   303  	}
   304  
   305  	if len(flagName) == 0 {
   306  		// Not doing flag completion
   307  		return nil, trimmedArgs, lastArg, nil
   308  	}
   309  
   310  	flag := findFlag(finalCmd, flagName)
   311  	if flag == nil {
   312  		// Flag not supported by this command, nothing to complete
   313  		err := fmt.Errorf("Subcommand '%s' does not support flag '%s'", finalCmd.Name(), flagName)
   314  		return nil, nil, "", err
   315  	}
   316  
   317  	if !flagWithEqual {
   318  		if len(flag.NoOptDefVal) != 0 {
   319  			// We had assumed dealing with a two-word flag but the flag is a boolean flag.
   320  			// In that case, there is no value following it, so we are not really doing flag completion.
   321  			// Reset everything to do argument completion.
   322  			trimmedArgs = args
   323  			flag = nil
   324  		}
   325  	}
   326  
   327  	return flag, trimmedArgs, lastArg, nil
   328  }
   329  
   330  func findFlag(cmd *cobra.Command, name string) *pflag.Flag {
   331  	flagSet := cmd.Flags()
   332  	if len(name) == 1 {
   333  		// First convert the short flag into a long flag
   334  		// as the cmd.Flag() search only accepts long flags
   335  		if short := flagSet.ShorthandLookup(name); short != nil {
   336  			CompDebugln(fmt.Sprintf("checkIfFlagCompletion: found flag '%s' which we will change to '%s'", name, short.Name))
   337  			name = short.Name
   338  		} else {
   339  			set := cmd.InheritedFlags()
   340  			if short = set.ShorthandLookup(name); short != nil {
   341  				CompDebugln(fmt.Sprintf("checkIfFlagCompletion: found inherited flag '%s' which we will change to '%s'", name, short.Name))
   342  				name = short.Name
   343  			} else {
   344  				return nil
   345  			}
   346  		}
   347  	}
   348  	return cmd.Flag(name)
   349  }
   350  
   351  // CompDebug prints the specified string to the same file as where the
   352  // completion script prints its logs.
   353  // Note that completion printouts should never be on stdout as they would
   354  // be wrongly interpreted as actual completion choices by the completion script.
   355  func CompDebug(msg string) {
   356  	msg = fmt.Sprintf("[Debug] %s", msg)
   357  
   358  	// Such logs are only printed when the user has set the environment
   359  	// variable BASH_COMP_DEBUG_FILE to the path of some file to be used.
   360  	if path := os.Getenv("BASH_COMP_DEBUG_FILE"); path != "" {
   361  		f, err := os.OpenFile(path,
   362  			os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
   363  		if err == nil {
   364  			defer f.Close()
   365  			f.WriteString(msg)
   366  		}
   367  	}
   368  
   369  	if debug {
   370  		// Must print to stderr for this not to be read by the completion script.
   371  		fmt.Fprintln(os.Stderr, msg)
   372  	}
   373  }
   374  
   375  // CompDebugln prints the specified string with a newline at the end
   376  // to the same file as where the completion script prints its logs.
   377  // Such logs are only printed when the user has set the environment
   378  // variable BASH_COMP_DEBUG_FILE to the path of some file to be used.
   379  func CompDebugln(msg string) {
   380  	CompDebug(fmt.Sprintf("%s\n", msg))
   381  }
   382  
   383  // CompError prints the specified completion message to stderr.
   384  func CompError(msg string) {
   385  	msg = fmt.Sprintf("[Error] %s", msg)
   386  
   387  	CompDebug(msg)
   388  
   389  	// If not already printed by the call to CompDebug().
   390  	if !debug {
   391  		// Must print to stderr for this not to be read by the completion script.
   392  		fmt.Fprintln(os.Stderr, msg)
   393  	}
   394  }
   395  
   396  // CompErrorln prints the specified completion message to stderr with a newline at the end.
   397  func CompErrorln(msg string) {
   398  	CompError(fmt.Sprintf("%s\n", msg))
   399  }