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 }