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