]>
Commit | Line | Data |
---|---|---|
15c0b25d AP |
1 | package cli |
2 | ||
3 | import ( | |
4 | "fmt" | |
5 | "io" | |
6 | "os" | |
7 | "regexp" | |
8 | "sort" | |
9 | "strings" | |
10 | "sync" | |
11 | "text/template" | |
12 | ||
13 | "github.com/armon/go-radix" | |
14 | "github.com/posener/complete" | |
15 | ) | |
16 | ||
17 | // CLI contains the state necessary to run subcommands and parse the | |
18 | // command line arguments. | |
19 | // | |
20 | // CLI also supports nested subcommands, such as "cli foo bar". To use | |
21 | // nested subcommands, the key in the Commands mapping below contains the | |
22 | // full subcommand. In this example, it would be "foo bar". | |
23 | // | |
24 | // If you use a CLI with nested subcommands, some semantics change due to | |
25 | // ambiguities: | |
26 | // | |
27 | // * We use longest prefix matching to find a matching subcommand. This | |
28 | // means if you register "foo bar" and the user executes "cli foo qux", | |
29 | // the "foo" command will be executed with the arg "qux". It is up to | |
30 | // you to handle these args. One option is to just return the special | |
31 | // help return code `RunResultHelp` to display help and exit. | |
32 | // | |
33 | // * The help flag "-h" or "-help" will look at all args to determine | |
34 | // the help function. For example: "otto apps list -h" will show the | |
35 | // help for "apps list" but "otto apps -h" will show it for "apps". | |
36 | // In the normal CLI, only the first subcommand is used. | |
37 | // | |
38 | // * The help flag will list any subcommands that a command takes | |
39 | // as well as the command's help itself. If there are no subcommands, | |
40 | // it will note this. If the CLI itself has no subcommands, this entire | |
41 | // section is omitted. | |
42 | // | |
43 | // * Any parent commands that don't exist are automatically created as | |
44 | // no-op commands that just show help for other subcommands. For example, | |
45 | // if you only register "foo bar", then "foo" is automatically created. | |
46 | // | |
47 | type CLI struct { | |
48 | // Args is the list of command-line arguments received excluding | |
49 | // the name of the app. For example, if the command "./cli foo bar" | |
50 | // was invoked, then Args should be []string{"foo", "bar"}. | |
51 | Args []string | |
52 | ||
53 | // Commands is a mapping of subcommand names to a factory function | |
54 | // for creating that Command implementation. If there is a command | |
55 | // with a blank string "", then it will be used as the default command | |
56 | // if no subcommand is specified. | |
57 | // | |
58 | // If the key has a space in it, this will create a nested subcommand. | |
59 | // For example, if the key is "foo bar", then to access it our CLI | |
60 | // must be accessed with "./cli foo bar". See the docs for CLI for | |
61 | // notes on how this changes some other behavior of the CLI as well. | |
62 | // | |
63 | // The factory should be as cheap as possible, ideally only allocating | |
64 | // a struct. The factory may be called multiple times in the course | |
65 | // of a command execution and certain events such as help require the | |
66 | // instantiation of all commands. Expensive initialization should be | |
67 | // deferred to function calls within the interface implementation. | |
68 | Commands map[string]CommandFactory | |
69 | ||
70 | // HiddenCommands is a list of commands that are "hidden". Hidden | |
71 | // commands are not given to the help function callback and do not | |
72 | // show up in autocomplete. The values in the slice should be equivalent | |
73 | // to the keys in the command map. | |
74 | HiddenCommands []string | |
75 | ||
76 | // Name defines the name of the CLI. | |
77 | Name string | |
78 | ||
79 | // Version of the CLI. | |
80 | Version string | |
81 | ||
82 | // Autocomplete enables or disables subcommand auto-completion support. | |
83 | // This is enabled by default when NewCLI is called. Otherwise, this | |
84 | // must enabled explicitly. | |
85 | // | |
86 | // Autocomplete requires the "Name" option to be set on CLI. This name | |
87 | // should be set exactly to the binary name that is autocompleted. | |
88 | // | |
89 | // Autocompletion is supported via the github.com/posener/complete | |
107c1cdb | 90 | // library. This library supports bash, zsh and fish. To add support |
15c0b25d AP |
91 | // for other shells, please see that library. |
92 | // | |
93 | // AutocompleteInstall and AutocompleteUninstall are the global flag | |
94 | // names for installing and uninstalling the autocompletion handlers | |
95 | // for the user's shell. The flag should omit the hyphen(s) in front of | |
96 | // the value. Both single and double hyphens will automatically be supported | |
97 | // for the flag name. These default to `autocomplete-install` and | |
98 | // `autocomplete-uninstall` respectively. | |
99 | // | |
100 | // AutocompleteNoDefaultFlags is a boolean which controls if the default auto- | |
101 | // complete flags like -help and -version are added to the output. | |
102 | // | |
103 | // AutocompleteGlobalFlags are a mapping of global flags for | |
104 | // autocompletion. The help and version flags are automatically added. | |
105 | Autocomplete bool | |
106 | AutocompleteInstall string | |
107 | AutocompleteUninstall string | |
108 | AutocompleteNoDefaultFlags bool | |
109 | AutocompleteGlobalFlags complete.Flags | |
110 | autocompleteInstaller autocompleteInstaller // For tests | |
111 | ||
112 | // HelpFunc and HelpWriter are used to output help information, if | |
113 | // requested. | |
114 | // | |
115 | // HelpFunc is the function called to generate the generic help | |
116 | // text that is shown if help must be shown for the CLI that doesn't | |
117 | // pertain to a specific command. | |
118 | // | |
119 | // HelpWriter is the Writer where the help text is outputted to. If | |
120 | // not specified, it will default to Stderr. | |
121 | HelpFunc HelpFunc | |
122 | HelpWriter io.Writer | |
123 | ||
124 | //--------------------------------------------------------------- | |
125 | // Internal fields set automatically | |
126 | ||
127 | once sync.Once | |
128 | autocomplete *complete.Complete | |
129 | commandTree *radix.Tree | |
130 | commandNested bool | |
131 | commandHidden map[string]struct{} | |
132 | subcommand string | |
133 | subcommandArgs []string | |
134 | topFlags []string | |
135 | ||
136 | // These are true when special global flags are set. We can/should | |
137 | // probably use a bitset for this one day. | |
138 | isHelp bool | |
139 | isVersion bool | |
140 | isAutocompleteInstall bool | |
141 | isAutocompleteUninstall bool | |
142 | } | |
143 | ||
144 | // NewClI returns a new CLI instance with sensible defaults. | |
145 | func NewCLI(app, version string) *CLI { | |
146 | return &CLI{ | |
147 | Name: app, | |
148 | Version: version, | |
149 | HelpFunc: BasicHelpFunc(app), | |
150 | Autocomplete: true, | |
151 | } | |
152 | ||
153 | } | |
154 | ||
155 | // IsHelp returns whether or not the help flag is present within the | |
156 | // arguments. | |
157 | func (c *CLI) IsHelp() bool { | |
158 | c.once.Do(c.init) | |
159 | return c.isHelp | |
160 | } | |
161 | ||
162 | // IsVersion returns whether or not the version flag is present within the | |
163 | // arguments. | |
164 | func (c *CLI) IsVersion() bool { | |
165 | c.once.Do(c.init) | |
166 | return c.isVersion | |
167 | } | |
168 | ||
169 | // Run runs the actual CLI based on the arguments given. | |
170 | func (c *CLI) Run() (int, error) { | |
171 | c.once.Do(c.init) | |
172 | ||
173 | // If this is a autocompletion request, satisfy it. This must be called | |
174 | // first before anything else since its possible to be autocompleting | |
175 | // -help or -version or other flags and we want to show completions | |
176 | // and not actually write the help or version. | |
177 | if c.Autocomplete && c.autocomplete.Complete() { | |
178 | return 0, nil | |
179 | } | |
180 | ||
181 | // Just show the version and exit if instructed. | |
182 | if c.IsVersion() && c.Version != "" { | |
183 | c.HelpWriter.Write([]byte(c.Version + "\n")) | |
184 | return 0, nil | |
185 | } | |
186 | ||
187 | // Just print the help when only '-h' or '--help' is passed. | |
188 | if c.IsHelp() && c.Subcommand() == "" { | |
189 | c.HelpWriter.Write([]byte(c.HelpFunc(c.helpCommands(c.Subcommand())) + "\n")) | |
190 | return 0, nil | |
191 | } | |
192 | ||
193 | // If we're attempting to install or uninstall autocomplete then handle | |
194 | if c.Autocomplete { | |
195 | // Autocomplete requires the "Name" to be set so that we know what | |
196 | // command to setup the autocomplete on. | |
197 | if c.Name == "" { | |
198 | return 1, fmt.Errorf( | |
199 | "internal error: CLI.Name must be specified for autocomplete to work") | |
200 | } | |
201 | ||
202 | // If both install and uninstall flags are specified, then error | |
203 | if c.isAutocompleteInstall && c.isAutocompleteUninstall { | |
204 | return 1, fmt.Errorf( | |
205 | "Either the autocomplete install or uninstall flag may " + | |
206 | "be specified, but not both.") | |
207 | } | |
208 | ||
209 | // If the install flag is specified, perform the install or uninstall | |
210 | if c.isAutocompleteInstall { | |
211 | if err := c.autocompleteInstaller.Install(c.Name); err != nil { | |
212 | return 1, err | |
213 | } | |
214 | ||
215 | return 0, nil | |
216 | } | |
217 | ||
218 | if c.isAutocompleteUninstall { | |
219 | if err := c.autocompleteInstaller.Uninstall(c.Name); err != nil { | |
220 | return 1, err | |
221 | } | |
222 | ||
223 | return 0, nil | |
224 | } | |
225 | } | |
226 | ||
227 | // Attempt to get the factory function for creating the command | |
228 | // implementation. If the command is invalid or blank, it is an error. | |
229 | raw, ok := c.commandTree.Get(c.Subcommand()) | |
230 | if !ok { | |
231 | c.HelpWriter.Write([]byte(c.HelpFunc(c.helpCommands(c.subcommandParent())) + "\n")) | |
232 | return 127, nil | |
233 | } | |
234 | ||
235 | command, err := raw.(CommandFactory)() | |
236 | if err != nil { | |
237 | return 1, err | |
238 | } | |
239 | ||
240 | // If we've been instructed to just print the help, then print it | |
241 | if c.IsHelp() { | |
242 | c.commandHelp(command) | |
243 | return 0, nil | |
244 | } | |
245 | ||
246 | // If there is an invalid flag, then error | |
247 | if len(c.topFlags) > 0 { | |
248 | c.HelpWriter.Write([]byte( | |
249 | "Invalid flags before the subcommand. If these flags are for\n" + | |
250 | "the subcommand, please put them after the subcommand.\n\n")) | |
251 | c.commandHelp(command) | |
252 | return 1, nil | |
253 | } | |
254 | ||
255 | code := command.Run(c.SubcommandArgs()) | |
256 | if code == RunResultHelp { | |
257 | // Requesting help | |
258 | c.commandHelp(command) | |
259 | return 1, nil | |
260 | } | |
261 | ||
262 | return code, nil | |
263 | } | |
264 | ||
265 | // Subcommand returns the subcommand that the CLI would execute. For | |
266 | // example, a CLI from "--version version --help" would return a Subcommand | |
267 | // of "version" | |
268 | func (c *CLI) Subcommand() string { | |
269 | c.once.Do(c.init) | |
270 | return c.subcommand | |
271 | } | |
272 | ||
273 | // SubcommandArgs returns the arguments that will be passed to the | |
274 | // subcommand. | |
275 | func (c *CLI) SubcommandArgs() []string { | |
276 | c.once.Do(c.init) | |
277 | return c.subcommandArgs | |
278 | } | |
279 | ||
280 | // subcommandParent returns the parent of this subcommand, if there is one. | |
281 | // If there isn't on, "" is returned. | |
282 | func (c *CLI) subcommandParent() string { | |
283 | // Get the subcommand, if it is "" alread just return | |
284 | sub := c.Subcommand() | |
285 | if sub == "" { | |
286 | return sub | |
287 | } | |
288 | ||
289 | // Clear any trailing spaces and find the last space | |
290 | sub = strings.TrimRight(sub, " ") | |
291 | idx := strings.LastIndex(sub, " ") | |
292 | ||
293 | if idx == -1 { | |
294 | // No space means our parent is root | |
295 | return "" | |
296 | } | |
297 | ||
298 | return sub[:idx] | |
299 | } | |
300 | ||
301 | func (c *CLI) init() { | |
302 | if c.HelpFunc == nil { | |
303 | c.HelpFunc = BasicHelpFunc("app") | |
304 | ||
305 | if c.Name != "" { | |
306 | c.HelpFunc = BasicHelpFunc(c.Name) | |
307 | } | |
308 | } | |
309 | ||
310 | if c.HelpWriter == nil { | |
311 | c.HelpWriter = os.Stderr | |
312 | } | |
313 | ||
314 | // Build our hidden commands | |
315 | if len(c.HiddenCommands) > 0 { | |
316 | c.commandHidden = make(map[string]struct{}) | |
317 | for _, h := range c.HiddenCommands { | |
318 | c.commandHidden[h] = struct{}{} | |
319 | } | |
320 | } | |
321 | ||
322 | // Build our command tree | |
323 | c.commandTree = radix.New() | |
324 | c.commandNested = false | |
325 | for k, v := range c.Commands { | |
326 | k = strings.TrimSpace(k) | |
327 | c.commandTree.Insert(k, v) | |
328 | if strings.ContainsRune(k, ' ') { | |
329 | c.commandNested = true | |
330 | } | |
331 | } | |
332 | ||
333 | // Go through the key and fill in any missing parent commands | |
334 | if c.commandNested { | |
335 | var walkFn radix.WalkFn | |
336 | toInsert := make(map[string]struct{}) | |
337 | walkFn = func(k string, raw interface{}) bool { | |
338 | idx := strings.LastIndex(k, " ") | |
339 | if idx == -1 { | |
340 | // If there is no space, just ignore top level commands | |
341 | return false | |
342 | } | |
343 | ||
344 | // Trim up to that space so we can get the expected parent | |
345 | k = k[:idx] | |
346 | if _, ok := c.commandTree.Get(k); ok { | |
347 | // Yay we have the parent! | |
348 | return false | |
349 | } | |
350 | ||
351 | // We're missing the parent, so let's insert this | |
352 | toInsert[k] = struct{}{} | |
353 | ||
354 | // Call the walk function recursively so we check this one too | |
355 | return walkFn(k, nil) | |
356 | } | |
357 | ||
358 | // Walk! | |
359 | c.commandTree.Walk(walkFn) | |
360 | ||
361 | // Insert any that we're missing | |
362 | for k := range toInsert { | |
363 | var f CommandFactory = func() (Command, error) { | |
364 | return &MockCommand{ | |
365 | HelpText: "This command is accessed by using one of the subcommands below.", | |
366 | RunResult: RunResultHelp, | |
367 | }, nil | |
368 | } | |
369 | ||
370 | c.commandTree.Insert(k, f) | |
371 | } | |
372 | } | |
373 | ||
374 | // Setup autocomplete if we have it enabled. We have to do this after | |
375 | // the command tree is setup so we can use the radix tree to easily find | |
376 | // all subcommands. | |
377 | if c.Autocomplete { | |
378 | c.initAutocomplete() | |
379 | } | |
380 | ||
381 | // Process the args | |
382 | c.processArgs() | |
383 | } | |
384 | ||
385 | func (c *CLI) initAutocomplete() { | |
386 | if c.AutocompleteInstall == "" { | |
387 | c.AutocompleteInstall = defaultAutocompleteInstall | |
388 | } | |
389 | ||
390 | if c.AutocompleteUninstall == "" { | |
391 | c.AutocompleteUninstall = defaultAutocompleteUninstall | |
392 | } | |
393 | ||
394 | if c.autocompleteInstaller == nil { | |
395 | c.autocompleteInstaller = &realAutocompleteInstaller{} | |
396 | } | |
397 | ||
398 | // Build the root command | |
399 | cmd := c.initAutocompleteSub("") | |
400 | ||
401 | // For the root, we add the global flags to the "Flags". This way | |
402 | // they don't show up on every command. | |
403 | if !c.AutocompleteNoDefaultFlags { | |
404 | cmd.Flags = map[string]complete.Predictor{ | |
405 | "-" + c.AutocompleteInstall: complete.PredictNothing, | |
406 | "-" + c.AutocompleteUninstall: complete.PredictNothing, | |
407 | "-help": complete.PredictNothing, | |
408 | "-version": complete.PredictNothing, | |
409 | } | |
410 | } | |
411 | cmd.GlobalFlags = c.AutocompleteGlobalFlags | |
412 | ||
413 | c.autocomplete = complete.New(c.Name, cmd) | |
414 | } | |
415 | ||
416 | // initAutocompleteSub creates the complete.Command for a subcommand with | |
417 | // the given prefix. This will continue recursively for all subcommands. | |
418 | // The prefix "" (empty string) can be used for the root command. | |
419 | func (c *CLI) initAutocompleteSub(prefix string) complete.Command { | |
420 | var cmd complete.Command | |
421 | walkFn := func(k string, raw interface{}) bool { | |
107c1cdb ND |
422 | // Ignore the empty key which can be present for default commands. |
423 | if k == "" { | |
424 | return false | |
425 | } | |
426 | ||
15c0b25d AP |
427 | // Keep track of the full key so that we can nest further if necessary |
428 | fullKey := k | |
429 | ||
430 | if len(prefix) > 0 { | |
431 | // If we have a prefix, trim the prefix + 1 (for the space) | |
432 | // Example: turns "sub one" to "one" with prefix "sub" | |
433 | k = k[len(prefix)+1:] | |
434 | } | |
435 | ||
436 | if idx := strings.Index(k, " "); idx >= 0 { | |
437 | // If there is a space, we trim up to the space. This turns | |
438 | // "sub sub2 sub3" into "sub". The prefix trim above will | |
439 | // trim our current depth properly. | |
440 | k = k[:idx] | |
441 | } | |
442 | ||
443 | if _, ok := cmd.Sub[k]; ok { | |
444 | // If we already tracked this subcommand then ignore | |
445 | return false | |
446 | } | |
447 | ||
448 | // If the command is hidden, don't record it at all | |
449 | if _, ok := c.commandHidden[fullKey]; ok { | |
450 | return false | |
451 | } | |
452 | ||
453 | if cmd.Sub == nil { | |
454 | cmd.Sub = complete.Commands(make(map[string]complete.Command)) | |
455 | } | |
456 | subCmd := c.initAutocompleteSub(fullKey) | |
457 | ||
458 | // Instantiate the command so that we can check if the command is | |
459 | // a CommandAutocomplete implementation. If there is an error | |
460 | // creating the command, we just ignore it since that will be caught | |
461 | // later. | |
462 | impl, err := raw.(CommandFactory)() | |
463 | if err != nil { | |
464 | impl = nil | |
465 | } | |
466 | ||
467 | // Check if it implements ComandAutocomplete. If so, setup the autocomplete | |
468 | if c, ok := impl.(CommandAutocomplete); ok { | |
469 | subCmd.Args = c.AutocompleteArgs() | |
470 | subCmd.Flags = c.AutocompleteFlags() | |
471 | } | |
472 | ||
473 | cmd.Sub[k] = subCmd | |
474 | return false | |
475 | } | |
476 | ||
477 | walkPrefix := prefix | |
478 | if walkPrefix != "" { | |
479 | walkPrefix += " " | |
480 | } | |
481 | ||
482 | c.commandTree.WalkPrefix(walkPrefix, walkFn) | |
483 | return cmd | |
484 | } | |
485 | ||
486 | func (c *CLI) commandHelp(command Command) { | |
487 | // Get the template to use | |
488 | tpl := strings.TrimSpace(defaultHelpTemplate) | |
489 | if t, ok := command.(CommandHelpTemplate); ok { | |
490 | tpl = t.HelpTemplate() | |
491 | } | |
492 | if !strings.HasSuffix(tpl, "\n") { | |
493 | tpl += "\n" | |
494 | } | |
495 | ||
496 | // Parse it | |
497 | t, err := template.New("root").Parse(tpl) | |
498 | if err != nil { | |
499 | t = template.Must(template.New("root").Parse(fmt.Sprintf( | |
500 | "Internal error! Failed to parse command help template: %s\n", err))) | |
501 | } | |
502 | ||
503 | // Template data | |
504 | data := map[string]interface{}{ | |
505 | "Name": c.Name, | |
506 | "Help": command.Help(), | |
507 | } | |
508 | ||
509 | // Build subcommand list if we have it | |
510 | var subcommandsTpl []map[string]interface{} | |
511 | if c.commandNested { | |
512 | // Get the matching keys | |
513 | subcommands := c.helpCommands(c.Subcommand()) | |
514 | keys := make([]string, 0, len(subcommands)) | |
515 | for k := range subcommands { | |
516 | keys = append(keys, k) | |
517 | } | |
518 | ||
519 | // Sort the keys | |
520 | sort.Strings(keys) | |
521 | ||
522 | // Figure out the padding length | |
523 | var longest int | |
524 | for _, k := range keys { | |
525 | if v := len(k); v > longest { | |
526 | longest = v | |
527 | } | |
528 | } | |
529 | ||
530 | // Go through and create their structures | |
531 | subcommandsTpl = make([]map[string]interface{}, 0, len(subcommands)) | |
532 | for _, k := range keys { | |
533 | // Get the command | |
534 | raw, ok := subcommands[k] | |
535 | if !ok { | |
536 | c.HelpWriter.Write([]byte(fmt.Sprintf( | |
537 | "Error getting subcommand %q", k))) | |
538 | } | |
539 | sub, err := raw() | |
540 | if err != nil { | |
541 | c.HelpWriter.Write([]byte(fmt.Sprintf( | |
542 | "Error instantiating %q: %s", k, err))) | |
543 | } | |
544 | ||
545 | // Find the last space and make sure we only include that last part | |
546 | name := k | |
547 | if idx := strings.LastIndex(k, " "); idx > -1 { | |
548 | name = name[idx+1:] | |
549 | } | |
550 | ||
551 | subcommandsTpl = append(subcommandsTpl, map[string]interface{}{ | |
552 | "Name": name, | |
553 | "NameAligned": name + strings.Repeat(" ", longest-len(k)), | |
554 | "Help": sub.Help(), | |
555 | "Synopsis": sub.Synopsis(), | |
556 | }) | |
557 | } | |
558 | } | |
559 | data["Subcommands"] = subcommandsTpl | |
560 | ||
561 | // Write | |
562 | err = t.Execute(c.HelpWriter, data) | |
563 | if err == nil { | |
564 | return | |
565 | } | |
566 | ||
567 | // An error, just output... | |
568 | c.HelpWriter.Write([]byte(fmt.Sprintf( | |
569 | "Internal error rendering help: %s", err))) | |
570 | } | |
571 | ||
572 | // helpCommands returns the subcommands for the HelpFunc argument. | |
573 | // This will only contain immediate subcommands. | |
574 | func (c *CLI) helpCommands(prefix string) map[string]CommandFactory { | |
575 | // If our prefix isn't empty, make sure it ends in ' ' | |
576 | if prefix != "" && prefix[len(prefix)-1] != ' ' { | |
577 | prefix += " " | |
578 | } | |
579 | ||
580 | // Get all the subkeys of this command | |
581 | var keys []string | |
582 | c.commandTree.WalkPrefix(prefix, func(k string, raw interface{}) bool { | |
583 | // Ignore any sub-sub keys, i.e. "foo bar baz" when we want "foo bar" | |
584 | if !strings.Contains(k[len(prefix):], " ") { | |
585 | keys = append(keys, k) | |
586 | } | |
587 | ||
588 | return false | |
589 | }) | |
590 | ||
591 | // For each of the keys return that in the map | |
592 | result := make(map[string]CommandFactory, len(keys)) | |
593 | for _, k := range keys { | |
594 | raw, ok := c.commandTree.Get(k) | |
595 | if !ok { | |
596 | // We just got it via WalkPrefix above, so we just panic | |
597 | panic("not found: " + k) | |
598 | } | |
599 | ||
600 | // If this is a hidden command, don't show it | |
601 | if _, ok := c.commandHidden[k]; ok { | |
602 | continue | |
603 | } | |
604 | ||
605 | result[k] = raw.(CommandFactory) | |
606 | } | |
607 | ||
608 | return result | |
609 | } | |
610 | ||
611 | func (c *CLI) processArgs() { | |
612 | for i, arg := range c.Args { | |
613 | if arg == "--" { | |
614 | break | |
615 | } | |
616 | ||
617 | // Check for help flags. | |
618 | if arg == "-h" || arg == "-help" || arg == "--help" { | |
619 | c.isHelp = true | |
620 | continue | |
621 | } | |
622 | ||
623 | // Check for autocomplete flags | |
624 | if c.Autocomplete { | |
625 | if arg == "-"+c.AutocompleteInstall || arg == "--"+c.AutocompleteInstall { | |
626 | c.isAutocompleteInstall = true | |
627 | continue | |
628 | } | |
629 | ||
630 | if arg == "-"+c.AutocompleteUninstall || arg == "--"+c.AutocompleteUninstall { | |
631 | c.isAutocompleteUninstall = true | |
632 | continue | |
633 | } | |
634 | } | |
635 | ||
636 | if c.subcommand == "" { | |
637 | // Check for version flags if not in a subcommand. | |
638 | if arg == "-v" || arg == "-version" || arg == "--version" { | |
639 | c.isVersion = true | |
640 | continue | |
641 | } | |
642 | ||
643 | if arg != "" && arg[0] == '-' { | |
644 | // Record the arg... | |
645 | c.topFlags = append(c.topFlags, arg) | |
646 | } | |
647 | } | |
648 | ||
649 | // If we didn't find a subcommand yet and this is the first non-flag | |
650 | // argument, then this is our subcommand. | |
651 | if c.subcommand == "" && arg != "" && arg[0] != '-' { | |
652 | c.subcommand = arg | |
653 | if c.commandNested { | |
654 | // If the command has a space in it, then it is invalid. | |
655 | // Set a blank command so that it fails. | |
656 | if strings.ContainsRune(arg, ' ') { | |
657 | c.subcommand = "" | |
658 | return | |
659 | } | |
660 | ||
661 | // Determine the argument we look to to end subcommands. | |
662 | // We look at all arguments until one has a space. This | |
663 | // disallows commands like: ./cli foo "bar baz". An argument | |
664 | // with a space is always an argument. | |
665 | j := 0 | |
666 | for k, v := range c.Args[i:] { | |
667 | if strings.ContainsRune(v, ' ') { | |
668 | break | |
669 | } | |
670 | ||
671 | j = i + k + 1 | |
672 | } | |
673 | ||
674 | // Nested CLI, the subcommand is actually the entire | |
675 | // arg list up to a flag that is still a valid subcommand. | |
676 | searchKey := strings.Join(c.Args[i:j], " ") | |
677 | k, _, ok := c.commandTree.LongestPrefix(searchKey) | |
678 | if ok { | |
679 | // k could be a prefix that doesn't contain the full | |
680 | // command such as "foo" instead of "foobar", so we | |
681 | // need to verify that we have an entire key. To do that, | |
682 | // we look for an ending in a space or an end of string. | |
683 | reVerify := regexp.MustCompile(regexp.QuoteMeta(k) + `( |$)`) | |
684 | if reVerify.MatchString(searchKey) { | |
685 | c.subcommand = k | |
686 | i += strings.Count(k, " ") | |
687 | } | |
688 | } | |
689 | } | |
690 | ||
691 | // The remaining args the subcommand arguments | |
692 | c.subcommandArgs = c.Args[i+1:] | |
693 | } | |
694 | } | |
695 | ||
696 | // If we never found a subcommand and support a default command, then | |
697 | // switch to using that. | |
698 | if c.subcommand == "" { | |
699 | if _, ok := c.Commands[""]; ok { | |
700 | args := c.topFlags | |
701 | args = append(args, c.subcommandArgs...) | |
702 | c.topFlags = nil | |
703 | c.subcommandArgs = args | |
704 | } | |
705 | } | |
706 | } | |
707 | ||
708 | // defaultAutocompleteInstall and defaultAutocompleteUninstall are the | |
709 | // default values for the autocomplete install and uninstall flags. | |
710 | const defaultAutocompleteInstall = "autocomplete-install" | |
711 | const defaultAutocompleteUninstall = "autocomplete-uninstall" | |
712 | ||
713 | const defaultHelpTemplate = ` | |
714 | {{.Help}}{{if gt (len .Subcommands) 0}} | |
715 | ||
716 | Subcommands: | |
717 | {{- range $value := .Subcommands }} | |
718 | {{ $value.NameAligned }} {{ $value.Synopsis }}{{ end }} | |
719 | {{- end }} | |
720 | ` |