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