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