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