podman

Форк
0
901 строка · 33.3 Кб
1
// Copyright 2013-2023 The Cobra Authors
2
//
3
// Licensed under the Apache License, Version 2.0 (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
//      http://www.apache.org/licenses/LICENSE-2.0
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14

15
package cobra
16

17
import (
18
	"fmt"
19
	"os"
20
	"strings"
21
	"sync"
22

23
	"github.com/spf13/pflag"
24
)
25

26
const (
27
	// ShellCompRequestCmd is the name of the hidden command that is used to request
28
	// completion results from the program.  It is used by the shell completion scripts.
29
	ShellCompRequestCmd = "__complete"
30
	// ShellCompNoDescRequestCmd is the name of the hidden command that is used to request
31
	// completion results without their description.  It is used by the shell completion scripts.
32
	ShellCompNoDescRequestCmd = "__completeNoDesc"
33
)
34

35
// Global map of flag completion functions. Make sure to use flagCompletionMutex before you try to read and write from it.
36
var flagCompletionFunctions = map[*pflag.Flag]func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective){}
37

38
// lock for reading and writing from flagCompletionFunctions
39
var flagCompletionMutex = &sync.RWMutex{}
40

41
// ShellCompDirective is a bit map representing the different behaviors the shell
42
// can be instructed to have once completions have been provided.
43
type ShellCompDirective int
44

45
type flagCompError struct {
46
	subCommand string
47
	flagName   string
48
}
49

50
func (e *flagCompError) Error() string {
51
	return "Subcommand '" + e.subCommand + "' does not support flag '" + e.flagName + "'"
52
}
53

54
const (
55
	// ShellCompDirectiveError indicates an error occurred and completions should be ignored.
56
	ShellCompDirectiveError ShellCompDirective = 1 << iota
57

58
	// ShellCompDirectiveNoSpace indicates that the shell should not add a space
59
	// after the completion even if there is a single completion provided.
60
	ShellCompDirectiveNoSpace
61

62
	// ShellCompDirectiveNoFileComp indicates that the shell should not provide
63
	// file completion even when no completion is provided.
64
	ShellCompDirectiveNoFileComp
65

66
	// ShellCompDirectiveFilterFileExt indicates that the provided completions
67
	// should be used as file extension filters.
68
	// For flags, using Command.MarkFlagFilename() and Command.MarkPersistentFlagFilename()
69
	// is a shortcut to using this directive explicitly.  The BashCompFilenameExt
70
	// annotation can also be used to obtain the same behavior for flags.
71
	ShellCompDirectiveFilterFileExt
72

73
	// ShellCompDirectiveFilterDirs indicates that only directory names should
74
	// be provided in file completion.  To request directory names within another
75
	// directory, the returned completions should specify the directory within
76
	// which to search.  The BashCompSubdirsInDir annotation can be used to
77
	// obtain the same behavior but only for flags.
78
	ShellCompDirectiveFilterDirs
79

80
	// ShellCompDirectiveKeepOrder indicates that the shell should preserve the order
81
	// in which the completions are provided
82
	ShellCompDirectiveKeepOrder
83

84
	// ===========================================================================
85

86
	// All directives using iota should be above this one.
87
	// For internal use.
88
	shellCompDirectiveMaxValue
89

90
	// ShellCompDirectiveDefault indicates to let the shell perform its default
91
	// behavior after completions have been provided.
92
	// This one must be last to avoid messing up the iota count.
93
	ShellCompDirectiveDefault ShellCompDirective = 0
94
)
95

96
const (
97
	// Constants for the completion command
98
	compCmdName              = "completion"
99
	compCmdNoDescFlagName    = "no-descriptions"
100
	compCmdNoDescFlagDesc    = "disable completion descriptions"
101
	compCmdNoDescFlagDefault = false
102
)
103

104
// CompletionOptions are the options to control shell completion
105
type CompletionOptions struct {
106
	// DisableDefaultCmd prevents Cobra from creating a default 'completion' command
107
	DisableDefaultCmd bool
108
	// DisableNoDescFlag prevents Cobra from creating the '--no-descriptions' flag
109
	// for shells that support completion descriptions
110
	DisableNoDescFlag bool
111
	// DisableDescriptions turns off all completion descriptions for shells
112
	// that support them
113
	DisableDescriptions bool
114
	// HiddenDefaultCmd makes the default 'completion' command hidden
115
	HiddenDefaultCmd bool
116
}
117

118
// NoFileCompletions can be used to disable file completion for commands that should
119
// not trigger file completions.
120
func NoFileCompletions(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
121
	return nil, ShellCompDirectiveNoFileComp
122
}
123

124
// FixedCompletions can be used to create a completion function which always
125
// returns the same results.
126
func FixedCompletions(choices []string, directive ShellCompDirective) func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
127
	return func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
128
		return choices, directive
129
	}
130
}
131

132
// RegisterFlagCompletionFunc should be called to register a function to provide completion for a flag.
133
func (c *Command) RegisterFlagCompletionFunc(flagName string, f func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective)) error {
134
	flag := c.Flag(flagName)
135
	if flag == nil {
136
		return fmt.Errorf("RegisterFlagCompletionFunc: flag '%s' does not exist", flagName)
137
	}
138
	flagCompletionMutex.Lock()
139
	defer flagCompletionMutex.Unlock()
140

141
	if _, exists := flagCompletionFunctions[flag]; exists {
142
		return fmt.Errorf("RegisterFlagCompletionFunc: flag '%s' already registered", flagName)
143
	}
144
	flagCompletionFunctions[flag] = f
145
	return nil
146
}
147

148
// GetFlagCompletionFunc returns the completion function for the given flag of the command, if available.
149
func (c *Command) GetFlagCompletionFunc(flagName string) (func(*Command, []string, string) ([]string, ShellCompDirective), bool) {
150
	flag := c.Flag(flagName)
151
	if flag == nil {
152
		return nil, false
153
	}
154

155
	flagCompletionMutex.RLock()
156
	defer flagCompletionMutex.RUnlock()
157

158
	completionFunc, exists := flagCompletionFunctions[flag]
159
	return completionFunc, exists
160
}
161

162
// Returns a string listing the different directive enabled in the specified parameter
163
func (d ShellCompDirective) string() string {
164
	var directives []string
165
	if d&ShellCompDirectiveError != 0 {
166
		directives = append(directives, "ShellCompDirectiveError")
167
	}
168
	if d&ShellCompDirectiveNoSpace != 0 {
169
		directives = append(directives, "ShellCompDirectiveNoSpace")
170
	}
171
	if d&ShellCompDirectiveNoFileComp != 0 {
172
		directives = append(directives, "ShellCompDirectiveNoFileComp")
173
	}
174
	if d&ShellCompDirectiveFilterFileExt != 0 {
175
		directives = append(directives, "ShellCompDirectiveFilterFileExt")
176
	}
177
	if d&ShellCompDirectiveFilterDirs != 0 {
178
		directives = append(directives, "ShellCompDirectiveFilterDirs")
179
	}
180
	if d&ShellCompDirectiveKeepOrder != 0 {
181
		directives = append(directives, "ShellCompDirectiveKeepOrder")
182
	}
183
	if len(directives) == 0 {
184
		directives = append(directives, "ShellCompDirectiveDefault")
185
	}
186

187
	if d >= shellCompDirectiveMaxValue {
188
		return fmt.Sprintf("ERROR: unexpected ShellCompDirective value: %d", d)
189
	}
190
	return strings.Join(directives, ", ")
191
}
192

193
// initCompleteCmd adds a special hidden command that can be used to request custom completions.
194
func (c *Command) initCompleteCmd(args []string) {
195
	completeCmd := &Command{
196
		Use:                   fmt.Sprintf("%s [command-line]", ShellCompRequestCmd),
197
		Aliases:               []string{ShellCompNoDescRequestCmd},
198
		DisableFlagsInUseLine: true,
199
		Hidden:                true,
200
		DisableFlagParsing:    true,
201
		Args:                  MinimumNArgs(1),
202
		Short:                 "Request shell completion choices for the specified command-line",
203
		Long: fmt.Sprintf("%[2]s is a special command that is used by the shell completion logic\n%[1]s",
204
			"to request completion choices for the specified command-line.", ShellCompRequestCmd),
205
		Run: func(cmd *Command, args []string) {
206
			finalCmd, completions, directive, err := cmd.getCompletions(args)
207
			if err != nil {
208
				CompErrorln(err.Error())
209
				// Keep going for multiple reasons:
210
				// 1- There could be some valid completions even though there was an error
211
				// 2- Even without completions, we need to print the directive
212
			}
213

214
			noDescriptions := (cmd.CalledAs() == ShellCompNoDescRequestCmd)
215
			for _, comp := range completions {
216
				if GetActiveHelpConfig(finalCmd) == activeHelpGlobalDisable {
217
					// Remove all activeHelp entries in this case
218
					if strings.HasPrefix(comp, activeHelpMarker) {
219
						continue
220
					}
221
				}
222
				if noDescriptions {
223
					// Remove any description that may be included following a tab character.
224
					comp = strings.Split(comp, "\t")[0]
225
				}
226

227
				// Make sure we only write the first line to the output.
228
				// This is needed if a description contains a linebreak.
229
				// Otherwise the shell scripts will interpret the other lines as new flags
230
				// and could therefore provide a wrong completion.
231
				comp = strings.Split(comp, "\n")[0]
232

233
				// Finally trim the completion.  This is especially important to get rid
234
				// of a trailing tab when there are no description following it.
235
				// For example, a sub-command without a description should not be completed
236
				// with a tab at the end (or else zsh will show a -- following it
237
				// although there is no description).
238
				comp = strings.TrimSpace(comp)
239

240
				// Print each possible completion to stdout for the completion script to consume.
241
				fmt.Fprintln(finalCmd.OutOrStdout(), comp)
242
			}
243

244
			// As the last printout, print the completion directive for the completion script to parse.
245
			// The directive integer must be that last character following a single colon (:).
246
			// The completion script expects :<directive>
247
			fmt.Fprintf(finalCmd.OutOrStdout(), ":%d\n", directive)
248

249
			// Print some helpful info to stderr for the user to understand.
250
			// Output from stderr must be ignored by the completion script.
251
			fmt.Fprintf(finalCmd.ErrOrStderr(), "Completion ended with directive: %s\n", directive.string())
252
		},
253
	}
254
	c.AddCommand(completeCmd)
255
	subCmd, _, err := c.Find(args)
256
	if err != nil || subCmd.Name() != ShellCompRequestCmd {
257
		// Only create this special command if it is actually being called.
258
		// This reduces possible side-effects of creating such a command;
259
		// for example, having this command would cause problems to a
260
		// cobra program that only consists of the root command, since this
261
		// command would cause the root command to suddenly have a subcommand.
262
		c.RemoveCommand(completeCmd)
263
	}
264
}
265

266
func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDirective, error) {
267
	// The last argument, which is not completely typed by the user,
268
	// should not be part of the list of arguments
269
	toComplete := args[len(args)-1]
270
	trimmedArgs := args[:len(args)-1]
271

272
	var finalCmd *Command
273
	var finalArgs []string
274
	var err error
275
	// Find the real command for which completion must be performed
276
	// check if we need to traverse here to parse local flags on parent commands
277
	if c.Root().TraverseChildren {
278
		finalCmd, finalArgs, err = c.Root().Traverse(trimmedArgs)
279
	} else {
280
		// For Root commands that don't specify any value for their Args fields, when we call
281
		// Find(), if those Root commands don't have any sub-commands, they will accept arguments.
282
		// However, because we have added the __complete sub-command in the current code path, the
283
		// call to Find() -> legacyArgs() will return an error if there are any arguments.
284
		// To avoid this, we first remove the __complete command to get back to having no sub-commands.
285
		rootCmd := c.Root()
286
		if len(rootCmd.Commands()) == 1 {
287
			rootCmd.RemoveCommand(c)
288
		}
289

290
		finalCmd, finalArgs, err = rootCmd.Find(trimmedArgs)
291
	}
292
	if err != nil {
293
		// Unable to find the real command. E.g., <program> someInvalidCmd <TAB>
294
		return c, []string{}, ShellCompDirectiveDefault, fmt.Errorf("Unable to find a command for arguments: %v", trimmedArgs)
295
	}
296
	finalCmd.ctx = c.ctx
297

298
	// These flags are normally added when `execute()` is called on `finalCmd`,
299
	// however, when doing completion, we don't call `finalCmd.execute()`.
300
	// Let's add the --help and --version flag ourselves but only if the finalCmd
301
	// has not disabled flag parsing; if flag parsing is disabled, it is up to the
302
	// finalCmd itself to handle the completion of *all* flags.
303
	if !finalCmd.DisableFlagParsing {
304
		finalCmd.InitDefaultHelpFlag()
305
		finalCmd.InitDefaultVersionFlag()
306
	}
307

308
	// Check if we are doing flag value completion before parsing the flags.
309
	// This is important because if we are completing a flag value, we need to also
310
	// remove the flag name argument from the list of finalArgs or else the parsing
311
	// could fail due to an invalid value (incomplete) for the flag.
312
	flag, finalArgs, toComplete, flagErr := checkIfFlagCompletion(finalCmd, finalArgs, toComplete)
313

314
	// Check if interspersed is false or -- was set on a previous arg.
315
	// This works by counting the arguments. Normally -- is not counted as arg but
316
	// if -- was already set or interspersed is false and there is already one arg then
317
	// the extra added -- is counted as arg.
318
	flagCompletion := true
319
	_ = finalCmd.ParseFlags(append(finalArgs, "--"))
320
	newArgCount := finalCmd.Flags().NArg()
321

322
	// Parse the flags early so we can check if required flags are set
323
	if err = finalCmd.ParseFlags(finalArgs); err != nil {
324
		return finalCmd, []string{}, ShellCompDirectiveDefault, fmt.Errorf("Error while parsing flags from args %v: %s", finalArgs, err.Error())
325
	}
326

327
	realArgCount := finalCmd.Flags().NArg()
328
	if newArgCount > realArgCount {
329
		// don't do flag completion (see above)
330
		flagCompletion = false
331
	}
332
	// Error while attempting to parse flags
333
	if flagErr != nil {
334
		// If error type is flagCompError and we don't want flagCompletion we should ignore the error
335
		if _, ok := flagErr.(*flagCompError); !(ok && !flagCompletion) {
336
			return finalCmd, []string{}, ShellCompDirectiveDefault, flagErr
337
		}
338
	}
339

340
	// Look for the --help or --version flags.  If they are present,
341
	// there should be no further completions.
342
	if helpOrVersionFlagPresent(finalCmd) {
343
		return finalCmd, []string{}, ShellCompDirectiveNoFileComp, nil
344
	}
345

346
	// We only remove the flags from the arguments if DisableFlagParsing is not set.
347
	// This is important for commands which have requested to do their own flag completion.
348
	if !finalCmd.DisableFlagParsing {
349
		finalArgs = finalCmd.Flags().Args()
350
	}
351

352
	if flag != nil && flagCompletion {
353
		// Check if we are completing a flag value subject to annotations
354
		if validExts, present := flag.Annotations[BashCompFilenameExt]; present {
355
			if len(validExts) != 0 {
356
				// File completion filtered by extensions
357
				return finalCmd, validExts, ShellCompDirectiveFilterFileExt, nil
358
			}
359

360
			// The annotation requests simple file completion.  There is no reason to do
361
			// that since it is the default behavior anyway.  Let's ignore this annotation
362
			// in case the program also registered a completion function for this flag.
363
			// Even though it is a mistake on the program's side, let's be nice when we can.
364
		}
365

366
		if subDir, present := flag.Annotations[BashCompSubdirsInDir]; present {
367
			if len(subDir) == 1 {
368
				// Directory completion from within a directory
369
				return finalCmd, subDir, ShellCompDirectiveFilterDirs, nil
370
			}
371
			// Directory completion
372
			return finalCmd, []string{}, ShellCompDirectiveFilterDirs, nil
373
		}
374
	}
375

376
	var completions []string
377
	var directive ShellCompDirective
378

379
	// Enforce flag groups before doing flag completions
380
	finalCmd.enforceFlagGroupsForCompletion()
381

382
	// Note that we want to perform flagname completion even if finalCmd.DisableFlagParsing==true;
383
	// doing this allows for completion of persistent flag names even for commands that disable flag parsing.
384
	//
385
	// When doing completion of a flag name, as soon as an argument starts with
386
	// a '-' we know it is a flag.  We cannot use isFlagArg() here as it requires
387
	// the flag name to be complete
388
	if flag == nil && len(toComplete) > 0 && toComplete[0] == '-' && !strings.Contains(toComplete, "=") && flagCompletion {
389
		// First check for required flags
390
		completions = completeRequireFlags(finalCmd, toComplete)
391

392
		// If we have not found any required flags, only then can we show regular flags
393
		if len(completions) == 0 {
394
			doCompleteFlags := func(flag *pflag.Flag) {
395
				if !flag.Changed ||
396
					strings.Contains(flag.Value.Type(), "Slice") ||
397
					strings.Contains(flag.Value.Type(), "Array") {
398
					// If the flag is not already present, or if it can be specified multiple times (Array or Slice)
399
					// we suggest it as a completion
400
					completions = append(completions, getFlagNameCompletions(flag, toComplete)...)
401
				}
402
			}
403

404
			// We cannot use finalCmd.Flags() because we may not have called ParsedFlags() for commands
405
			// that have set DisableFlagParsing; it is ParseFlags() that merges the inherited and
406
			// non-inherited flags.
407
			finalCmd.InheritedFlags().VisitAll(func(flag *pflag.Flag) {
408
				doCompleteFlags(flag)
409
			})
410
			// Try to complete non-inherited flags even if DisableFlagParsing==true.
411
			// This allows programs to tell Cobra about flags for completion even
412
			// if the actual parsing of flags is not done by Cobra.
413
			// For instance, Helm uses this to provide flag name completion for
414
			// some of its plugins.
415
			finalCmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) {
416
				doCompleteFlags(flag)
417
			})
418
		}
419

420
		directive = ShellCompDirectiveNoFileComp
421
		if len(completions) == 1 && strings.HasSuffix(completions[0], "=") {
422
			// If there is a single completion, the shell usually adds a space
423
			// after the completion.  We don't want that if the flag ends with an =
424
			directive = ShellCompDirectiveNoSpace
425
		}
426

427
		if !finalCmd.DisableFlagParsing {
428
			// If DisableFlagParsing==false, we have completed the flags as known by Cobra;
429
			// we can return what we found.
430
			// If DisableFlagParsing==true, Cobra may not be aware of all flags, so we
431
			// let the logic continue to see if ValidArgsFunction needs to be called.
432
			return finalCmd, completions, directive, nil
433
		}
434
	} else {
435
		directive = ShellCompDirectiveDefault
436
		if flag == nil {
437
			foundLocalNonPersistentFlag := false
438
			// If TraverseChildren is true on the root command we don't check for
439
			// local flags because we can use a local flag on a parent command
440
			if !finalCmd.Root().TraverseChildren {
441
				// Check if there are any local, non-persistent flags on the command-line
442
				localNonPersistentFlags := finalCmd.LocalNonPersistentFlags()
443
				finalCmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) {
444
					if localNonPersistentFlags.Lookup(flag.Name) != nil && flag.Changed {
445
						foundLocalNonPersistentFlag = true
446
					}
447
				})
448
			}
449

450
			// Complete subcommand names, including the help command
451
			if len(finalArgs) == 0 && !foundLocalNonPersistentFlag {
452
				// We only complete sub-commands if:
453
				// - there are no arguments on the command-line and
454
				// - there are no local, non-persistent flags on the command-line or TraverseChildren is true
455
				for _, subCmd := range finalCmd.Commands() {
456
					if subCmd.IsAvailableCommand() || subCmd == finalCmd.helpCommand {
457
						if strings.HasPrefix(subCmd.Name(), toComplete) {
458
							completions = append(completions, fmt.Sprintf("%s\t%s", subCmd.Name(), subCmd.Short))
459
						}
460
						directive = ShellCompDirectiveNoFileComp
461
					}
462
				}
463
			}
464

465
			// Complete required flags even without the '-' prefix
466
			completions = append(completions, completeRequireFlags(finalCmd, toComplete)...)
467

468
			// Always complete ValidArgs, even if we are completing a subcommand name.
469
			// This is for commands that have both subcommands and ValidArgs.
470
			if len(finalCmd.ValidArgs) > 0 {
471
				if len(finalArgs) == 0 {
472
					// ValidArgs are only for the first argument
473
					for _, validArg := range finalCmd.ValidArgs {
474
						if strings.HasPrefix(validArg, toComplete) {
475
							completions = append(completions, validArg)
476
						}
477
					}
478
					directive = ShellCompDirectiveNoFileComp
479

480
					// If no completions were found within commands or ValidArgs,
481
					// see if there are any ArgAliases that should be completed.
482
					if len(completions) == 0 {
483
						for _, argAlias := range finalCmd.ArgAliases {
484
							if strings.HasPrefix(argAlias, toComplete) {
485
								completions = append(completions, argAlias)
486
							}
487
						}
488
					}
489
				}
490

491
				// If there are ValidArgs specified (even if they don't match), we stop completion.
492
				// Only one of ValidArgs or ValidArgsFunction can be used for a single command.
493
				return finalCmd, completions, directive, nil
494
			}
495

496
			// Let the logic continue so as to add any ValidArgsFunction completions,
497
			// even if we already found sub-commands.
498
			// This is for commands that have subcommands but also specify a ValidArgsFunction.
499
		}
500
	}
501

502
	// Find the completion function for the flag or command
503
	var completionFn func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective)
504
	if flag != nil && flagCompletion {
505
		flagCompletionMutex.RLock()
506
		completionFn = flagCompletionFunctions[flag]
507
		flagCompletionMutex.RUnlock()
508
	} else {
509
		completionFn = finalCmd.ValidArgsFunction
510
	}
511
	if completionFn != nil {
512
		// Go custom completion defined for this flag or command.
513
		// Call the registered completion function to get the completions.
514
		var comps []string
515
		comps, directive = completionFn(finalCmd, finalArgs, toComplete)
516
		completions = append(completions, comps...)
517
	}
518

519
	return finalCmd, completions, directive, nil
520
}
521

522
func helpOrVersionFlagPresent(cmd *Command) bool {
523
	if versionFlag := cmd.Flags().Lookup("version"); versionFlag != nil &&
524
		len(versionFlag.Annotations[FlagSetByCobraAnnotation]) > 0 && versionFlag.Changed {
525
		return true
526
	}
527
	if helpFlag := cmd.Flags().Lookup("help"); helpFlag != nil &&
528
		len(helpFlag.Annotations[FlagSetByCobraAnnotation]) > 0 && helpFlag.Changed {
529
		return true
530
	}
531
	return false
532
}
533

534
func getFlagNameCompletions(flag *pflag.Flag, toComplete string) []string {
535
	if nonCompletableFlag(flag) {
536
		return []string{}
537
	}
538

539
	var completions []string
540
	flagName := "--" + flag.Name
541
	if strings.HasPrefix(flagName, toComplete) {
542
		// Flag without the =
543
		completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flag.Usage))
544

545
		// Why suggest both long forms: --flag and --flag= ?
546
		// This forces the user to *always* have to type either an = or a space after the flag name.
547
		// Let's be nice and avoid making users have to do that.
548
		// Since boolean flags and shortname flags don't show the = form, let's go that route and never show it.
549
		// The = form will still work, we just won't suggest it.
550
		// This also makes the list of suggested flags shorter as we avoid all the = forms.
551
		//
552
		// if len(flag.NoOptDefVal) == 0 {
553
		// 	// Flag requires a value, so it can be suffixed with =
554
		// 	flagName += "="
555
		// 	completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flag.Usage))
556
		// }
557
	}
558

559
	flagName = "-" + flag.Shorthand
560
	if len(flag.Shorthand) > 0 && strings.HasPrefix(flagName, toComplete) {
561
		completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flag.Usage))
562
	}
563

564
	return completions
565
}
566

567
func completeRequireFlags(finalCmd *Command, toComplete string) []string {
568
	var completions []string
569

570
	doCompleteRequiredFlags := func(flag *pflag.Flag) {
571
		if _, present := flag.Annotations[BashCompOneRequiredFlag]; present {
572
			if !flag.Changed {
573
				// If the flag is not already present, we suggest it as a completion
574
				completions = append(completions, getFlagNameCompletions(flag, toComplete)...)
575
			}
576
		}
577
	}
578

579
	// We cannot use finalCmd.Flags() because we may not have called ParsedFlags() for commands
580
	// that have set DisableFlagParsing; it is ParseFlags() that merges the inherited and
581
	// non-inherited flags.
582
	finalCmd.InheritedFlags().VisitAll(func(flag *pflag.Flag) {
583
		doCompleteRequiredFlags(flag)
584
	})
585
	finalCmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) {
586
		doCompleteRequiredFlags(flag)
587
	})
588

589
	return completions
590
}
591

592
func checkIfFlagCompletion(finalCmd *Command, args []string, lastArg string) (*pflag.Flag, []string, string, error) {
593
	if finalCmd.DisableFlagParsing {
594
		// We only do flag completion if we are allowed to parse flags
595
		// This is important for commands which have requested to do their own flag completion.
596
		return nil, args, lastArg, nil
597
	}
598

599
	var flagName string
600
	trimmedArgs := args
601
	flagWithEqual := false
602
	orgLastArg := lastArg
603

604
	// When doing completion of a flag name, as soon as an argument starts with
605
	// a '-' we know it is a flag.  We cannot use isFlagArg() here as that function
606
	// requires the flag name to be complete
607
	if len(lastArg) > 0 && lastArg[0] == '-' {
608
		if index := strings.Index(lastArg, "="); index >= 0 {
609
			// Flag with an =
610
			if strings.HasPrefix(lastArg[:index], "--") {
611
				// Flag has full name
612
				flagName = lastArg[2:index]
613
			} else {
614
				// Flag is shorthand
615
				// We have to get the last shorthand flag name
616
				// e.g. `-asd` => d to provide the correct completion
617
				// https://github.com/spf13/cobra/issues/1257
618
				flagName = lastArg[index-1 : index]
619
			}
620
			lastArg = lastArg[index+1:]
621
			flagWithEqual = true
622
		} else {
623
			// Normal flag completion
624
			return nil, args, lastArg, nil
625
		}
626
	}
627

628
	if len(flagName) == 0 {
629
		if len(args) > 0 {
630
			prevArg := args[len(args)-1]
631
			if isFlagArg(prevArg) {
632
				// Only consider the case where the flag does not contain an =.
633
				// If the flag contains an = it means it has already been fully processed,
634
				// so we don't need to deal with it here.
635
				if index := strings.Index(prevArg, "="); index < 0 {
636
					if strings.HasPrefix(prevArg, "--") {
637
						// Flag has full name
638
						flagName = prevArg[2:]
639
					} else {
640
						// Flag is shorthand
641
						// We have to get the last shorthand flag name
642
						// e.g. `-asd` => d to provide the correct completion
643
						// https://github.com/spf13/cobra/issues/1257
644
						flagName = prevArg[len(prevArg)-1:]
645
					}
646
					// Remove the uncompleted flag or else there could be an error created
647
					// for an invalid value for that flag
648
					trimmedArgs = args[:len(args)-1]
649
				}
650
			}
651
		}
652
	}
653

654
	if len(flagName) == 0 {
655
		// Not doing flag completion
656
		return nil, trimmedArgs, lastArg, nil
657
	}
658

659
	flag := findFlag(finalCmd, flagName)
660
	if flag == nil {
661
		// Flag not supported by this command, the interspersed option might be set so return the original args
662
		return nil, args, orgLastArg, &flagCompError{subCommand: finalCmd.Name(), flagName: flagName}
663
	}
664

665
	if !flagWithEqual {
666
		if len(flag.NoOptDefVal) != 0 {
667
			// We had assumed dealing with a two-word flag but the flag is a boolean flag.
668
			// In that case, there is no value following it, so we are not really doing flag completion.
669
			// Reset everything to do noun completion.
670
			trimmedArgs = args
671
			flag = nil
672
		}
673
	}
674

675
	return flag, trimmedArgs, lastArg, nil
676
}
677

678
// InitDefaultCompletionCmd adds a default 'completion' command to c.
679
// This function will do nothing if any of the following is true:
680
// 1- the feature has been explicitly disabled by the program,
681
// 2- c has no subcommands (to avoid creating one),
682
// 3- c already has a 'completion' command provided by the program.
683
func (c *Command) InitDefaultCompletionCmd() {
684
	if c.CompletionOptions.DisableDefaultCmd || !c.HasSubCommands() {
685
		return
686
	}
687

688
	for _, cmd := range c.commands {
689
		if cmd.Name() == compCmdName || cmd.HasAlias(compCmdName) {
690
			// A completion command is already available
691
			return
692
		}
693
	}
694

695
	haveNoDescFlag := !c.CompletionOptions.DisableNoDescFlag && !c.CompletionOptions.DisableDescriptions
696

697
	completionCmd := &Command{
698
		Use:   compCmdName,
699
		Short: "Generate the autocompletion script for the specified shell",
700
		Long: fmt.Sprintf(`Generate the autocompletion script for %[1]s for the specified shell.
701
See each sub-command's help for details on how to use the generated script.
702
`, c.Root().Name()),
703
		Args:              NoArgs,
704
		ValidArgsFunction: NoFileCompletions,
705
		Hidden:            c.CompletionOptions.HiddenDefaultCmd,
706
		GroupID:           c.completionCommandGroupID,
707
	}
708
	c.AddCommand(completionCmd)
709

710
	out := c.OutOrStdout()
711
	noDesc := c.CompletionOptions.DisableDescriptions
712
	shortDesc := "Generate the autocompletion script for %s"
713
	bash := &Command{
714
		Use:   "bash",
715
		Short: fmt.Sprintf(shortDesc, "bash"),
716
		Long: fmt.Sprintf(`Generate the autocompletion script for the bash shell.
717

718
This script depends on the 'bash-completion' package.
719
If it is not installed already, you can install it via your OS's package manager.
720

721
To load completions in your current shell session:
722

723
	source <(%[1]s completion bash)
724

725
To load completions for every new session, execute once:
726

727
#### Linux:
728

729
	%[1]s completion bash > /etc/bash_completion.d/%[1]s
730

731
#### macOS:
732

733
	%[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s
734

735
You will need to start a new shell for this setup to take effect.
736
`, c.Root().Name()),
737
		Args:                  NoArgs,
738
		DisableFlagsInUseLine: true,
739
		ValidArgsFunction:     NoFileCompletions,
740
		RunE: func(cmd *Command, args []string) error {
741
			return cmd.Root().GenBashCompletionV2(out, !noDesc)
742
		},
743
	}
744
	if haveNoDescFlag {
745
		bash.Flags().BoolVar(&noDesc, compCmdNoDescFlagName, compCmdNoDescFlagDefault, compCmdNoDescFlagDesc)
746
	}
747

748
	zsh := &Command{
749
		Use:   "zsh",
750
		Short: fmt.Sprintf(shortDesc, "zsh"),
751
		Long: fmt.Sprintf(`Generate the autocompletion script for the zsh shell.
752

753
If shell completion is not already enabled in your environment you will need
754
to enable it.  You can execute the following once:
755

756
	echo "autoload -U compinit; compinit" >> ~/.zshrc
757

758
To load completions in your current shell session:
759

760
	source <(%[1]s completion zsh)
761

762
To load completions for every new session, execute once:
763

764
#### Linux:
765

766
	%[1]s completion zsh > "${fpath[1]}/_%[1]s"
767

768
#### macOS:
769

770
	%[1]s completion zsh > $(brew --prefix)/share/zsh/site-functions/_%[1]s
771

772
You will need to start a new shell for this setup to take effect.
773
`, c.Root().Name()),
774
		Args:              NoArgs,
775
		ValidArgsFunction: NoFileCompletions,
776
		RunE: func(cmd *Command, args []string) error {
777
			if noDesc {
778
				return cmd.Root().GenZshCompletionNoDesc(out)
779
			}
780
			return cmd.Root().GenZshCompletion(out)
781
		},
782
	}
783
	if haveNoDescFlag {
784
		zsh.Flags().BoolVar(&noDesc, compCmdNoDescFlagName, compCmdNoDescFlagDefault, compCmdNoDescFlagDesc)
785
	}
786

787
	fish := &Command{
788
		Use:   "fish",
789
		Short: fmt.Sprintf(shortDesc, "fish"),
790
		Long: fmt.Sprintf(`Generate the autocompletion script for the fish shell.
791

792
To load completions in your current shell session:
793

794
	%[1]s completion fish | source
795

796
To load completions for every new session, execute once:
797

798
	%[1]s completion fish > ~/.config/fish/completions/%[1]s.fish
799

800
You will need to start a new shell for this setup to take effect.
801
`, c.Root().Name()),
802
		Args:              NoArgs,
803
		ValidArgsFunction: NoFileCompletions,
804
		RunE: func(cmd *Command, args []string) error {
805
			return cmd.Root().GenFishCompletion(out, !noDesc)
806
		},
807
	}
808
	if haveNoDescFlag {
809
		fish.Flags().BoolVar(&noDesc, compCmdNoDescFlagName, compCmdNoDescFlagDefault, compCmdNoDescFlagDesc)
810
	}
811

812
	powershell := &Command{
813
		Use:   "powershell",
814
		Short: fmt.Sprintf(shortDesc, "powershell"),
815
		Long: fmt.Sprintf(`Generate the autocompletion script for powershell.
816

817
To load completions in your current shell session:
818

819
	%[1]s completion powershell | Out-String | Invoke-Expression
820

821
To load completions for every new session, add the output of the above command
822
to your powershell profile.
823
`, c.Root().Name()),
824
		Args:              NoArgs,
825
		ValidArgsFunction: NoFileCompletions,
826
		RunE: func(cmd *Command, args []string) error {
827
			if noDesc {
828
				return cmd.Root().GenPowerShellCompletion(out)
829
			}
830
			return cmd.Root().GenPowerShellCompletionWithDesc(out)
831

832
		},
833
	}
834
	if haveNoDescFlag {
835
		powershell.Flags().BoolVar(&noDesc, compCmdNoDescFlagName, compCmdNoDescFlagDefault, compCmdNoDescFlagDesc)
836
	}
837

838
	completionCmd.AddCommand(bash, zsh, fish, powershell)
839
}
840

841
func findFlag(cmd *Command, name string) *pflag.Flag {
842
	flagSet := cmd.Flags()
843
	if len(name) == 1 {
844
		// First convert the short flag into a long flag
845
		// as the cmd.Flag() search only accepts long flags
846
		if short := flagSet.ShorthandLookup(name); short != nil {
847
			name = short.Name
848
		} else {
849
			set := cmd.InheritedFlags()
850
			if short = set.ShorthandLookup(name); short != nil {
851
				name = short.Name
852
			} else {
853
				return nil
854
			}
855
		}
856
	}
857
	return cmd.Flag(name)
858
}
859

860
// CompDebug prints the specified string to the same file as where the
861
// completion script prints its logs.
862
// Note that completion printouts should never be on stdout as they would
863
// be wrongly interpreted as actual completion choices by the completion script.
864
func CompDebug(msg string, printToStdErr bool) {
865
	msg = fmt.Sprintf("[Debug] %s", msg)
866

867
	// Such logs are only printed when the user has set the environment
868
	// variable BASH_COMP_DEBUG_FILE to the path of some file to be used.
869
	if path := os.Getenv("BASH_COMP_DEBUG_FILE"); path != "" {
870
		f, err := os.OpenFile(path,
871
			os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
872
		if err == nil {
873
			defer f.Close()
874
			WriteStringAndCheck(f, msg)
875
		}
876
	}
877

878
	if printToStdErr {
879
		// Must print to stderr for this not to be read by the completion script.
880
		fmt.Fprint(os.Stderr, msg)
881
	}
882
}
883

884
// CompDebugln prints the specified string with a newline at the end
885
// to the same file as where the completion script prints its logs.
886
// Such logs are only printed when the user has set the environment
887
// variable BASH_COMP_DEBUG_FILE to the path of some file to be used.
888
func CompDebugln(msg string, printToStdErr bool) {
889
	CompDebug(fmt.Sprintf("%s\n", msg), printToStdErr)
890
}
891

892
// CompError prints the specified completion message to stderr.
893
func CompError(msg string) {
894
	msg = fmt.Sprintf("[Error] %s", msg)
895
	CompDebug(msg, true)
896
}
897

898
// CompErrorln prints the specified completion message to stderr with a newline at the end.
899
func CompErrorln(msg string) {
900
	CompError(fmt.Sprintf("%s\n", msg))
901
}
902

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.