full diff: https://github.com/spf13/cobra/compare/v1.0.0...v1.1.1
Notable changes:
- Extend Go completions and revamp zsh comp
- Add completion for help command
- Complete subcommands when TraverseChildren is set
- Fix stderr printing functions
- fix: fish output redirection
- fix manpage building with new go-md2man
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
... | ... |
@@ -161,7 +161,7 @@ github.com/grpc-ecosystem/go-grpc-prometheus c225b8c3b01faf2899099b768856 |
161 | 161 |
github.com/cespare/xxhash/v2 d7df74196a9e781ede915320c11c378c1b2f3a1f # v2.1.1 |
162 | 162 |
|
163 | 163 |
# cli |
164 |
-github.com/spf13/cobra a684a6d7f5e37385d954dd3b5a14fc6912c6ab9d # v1.0.0 |
|
164 |
+github.com/spf13/cobra 86f8bfd7fef868a174e1b606783bd7f5c82ddf8f # v1.1.1 |
|
165 | 165 |
github.com/spf13/pflag 2e9d26c8c37aae03e3f9d4e90b7116f5accb7cab # v1.0.5 |
166 | 166 |
github.com/inconshreveable/mousetrap 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 # v1.0.0 |
167 | 167 |
github.com/morikuni/aec 39771216ff4c63d11f5e604076f9c45e8be1067b # v1.0.0 |
... | ... |
@@ -2,35 +2,14 @@ |
2 | 2 |
|
3 | 3 |
Cobra is both a library for creating powerful modern CLI applications as well as a program to generate applications and command files. |
4 | 4 |
|
5 |
-Many of the most widely used Go projects are built using Cobra, such as: |
|
6 |
-[Kubernetes](http://kubernetes.io/), |
|
7 |
-[Hugo](http://gohugo.io), |
|
8 |
-[rkt](https://github.com/coreos/rkt), |
|
9 |
-[etcd](https://github.com/coreos/etcd), |
|
10 |
-[Moby (former Docker)](https://github.com/moby/moby), |
|
11 |
-[Docker (distribution)](https://github.com/docker/distribution), |
|
12 |
-[OpenShift](https://www.openshift.com/), |
|
13 |
-[Delve](https://github.com/derekparker/delve), |
|
14 |
-[GopherJS](http://www.gopherjs.org/), |
|
15 |
-[CockroachDB](http://www.cockroachlabs.com/), |
|
16 |
-[Bleve](http://www.blevesearch.com/), |
|
17 |
-[ProjectAtomic (enterprise)](http://www.projectatomic.io/), |
|
18 |
-[Giant Swarm's gsctl](https://github.com/giantswarm/gsctl), |
|
19 |
-[Nanobox](https://github.com/nanobox-io/nanobox)/[Nanopack](https://github.com/nanopack), |
|
20 |
-[rclone](http://rclone.org/), |
|
21 |
-[nehm](https://github.com/bogem/nehm), |
|
22 |
-[Pouch](https://github.com/alibaba/pouch), |
|
23 |
-[Istio](https://istio.io), |
|
24 |
-[Prototool](https://github.com/uber/prototool), |
|
25 |
-[mattermost-server](https://github.com/mattermost/mattermost-server), |
|
26 |
-[Gardener](https://github.com/gardener/gardenctl), |
|
27 |
-[Linkerd](https://linkerd.io/), |
|
28 |
-[Github CLI](https://github.com/cli/cli) |
|
29 |
-etc. |
|
5 |
+Cobra is used in many Go projects such as [Kubernetes](http://kubernetes.io/), |
|
6 |
+[Hugo](https://gohugo.io), and [Github CLI](https://github.com/cli/cli) to |
|
7 |
+name a few. [This list](./projects_using_cobra.md) contains a more extensive list of projects using Cobra. |
|
30 | 8 |
|
31 | 9 |
[![Build Status](https://travis-ci.org/spf13/cobra.svg "Travis CI status")](https://travis-ci.org/spf13/cobra) |
32 | 10 |
[![GoDoc](https://godoc.org/github.com/spf13/cobra?status.svg)](https://godoc.org/github.com/spf13/cobra) |
33 | 11 |
[![Go Report Card](https://goreportcard.com/badge/github.com/spf13/cobra)](https://goreportcard.com/report/github.com/spf13/cobra) |
12 |
+[![Slack](https://img.shields.io/badge/Slack-cobra-brightgreen)](https://gophers.slack.com/archives/CD3LP1199) |
|
34 | 13 |
|
35 | 14 |
# Table of Contents |
36 | 15 |
|
... | ... |
@@ -50,9 +29,8 @@ etc. |
50 | 50 |
* [PreRun and PostRun Hooks](#prerun-and-postrun-hooks) |
51 | 51 |
* [Suggestions when "unknown command" happens](#suggestions-when-unknown-command-happens) |
52 | 52 |
* [Generating documentation for your command](#generating-documentation-for-your-command) |
53 |
- * [Generating bash completions](#generating-bash-completions) |
|
54 |
- * [Generating zsh completions](#generating-zsh-completions) |
|
55 |
-- [Contributing](#contributing) |
|
53 |
+ * [Generating shell completions](#generating-shell-completions) |
|
54 |
+- [Contributing](CONTRIBUTING.md) |
|
56 | 55 |
- [License](#license) |
57 | 56 |
|
58 | 57 |
# Overview |
... | ... |
@@ -72,7 +50,7 @@ Cobra provides: |
72 | 72 |
* Intelligent suggestions (`app srver`... did you mean `app server`?) |
73 | 73 |
* Automatic help generation for commands and flags |
74 | 74 |
* Automatic help flag recognition of `-h`, `--help`, etc. |
75 |
-* Automatically generated bash autocomplete for your application |
|
75 |
+* Automatically generated shell autocomplete for your application (bash, zsh, fish, powershell) |
|
76 | 76 |
* Automatically generated man pages for your application |
77 | 77 |
* Command aliases so you can change things without breaking them |
78 | 78 |
* The flexibility to define your own help, usage, etc. |
... | ... |
@@ -130,7 +108,7 @@ Using Cobra is easy. First, use `go get` to install the latest version |
130 | 130 |
of the library. This command will install the `cobra` generator executable |
131 | 131 |
along with the library and its dependencies: |
132 | 132 |
|
133 |
- go get -u github.com/spf13/cobra/cobra |
|
133 |
+ go get -u github.com/spf13/cobra |
|
134 | 134 |
|
135 | 135 |
Next, include Cobra in your application: |
136 | 136 |
|
... | ... |
@@ -199,7 +177,7 @@ var rootCmd = &cobra.Command{ |
199 | 199 |
|
200 | 200 |
func Execute() { |
201 | 201 |
if err := rootCmd.Execute(); err != nil { |
202 |
- fmt.Println(err) |
|
202 |
+ fmt.Fprintln(os.Stderr, err) |
|
203 | 203 |
os.Exit(1) |
204 | 204 |
} |
205 | 205 |
} |
... | ... |
@@ -335,6 +313,37 @@ var versionCmd = &cobra.Command{ |
335 | 335 |
} |
336 | 336 |
``` |
337 | 337 |
|
338 |
+### Returning and handling errors |
|
339 |
+ |
|
340 |
+If you wish to return an error to the caller of a command, `RunE` can be used. |
|
341 |
+ |
|
342 |
+```go |
|
343 |
+package cmd |
|
344 |
+ |
|
345 |
+import ( |
|
346 |
+ "fmt" |
|
347 |
+ |
|
348 |
+ "github.com/spf13/cobra" |
|
349 |
+) |
|
350 |
+ |
|
351 |
+func init() { |
|
352 |
+ rootCmd.AddCommand(tryCmd) |
|
353 |
+} |
|
354 |
+ |
|
355 |
+var tryCmd = &cobra.Command{ |
|
356 |
+ Use: "try", |
|
357 |
+ Short: "Try and possibly fail at something", |
|
358 |
+ RunE: func(cmd *cobra.Command, args []string) error { |
|
359 |
+ if err := someFunc(); err != nil { |
|
360 |
+ return err |
|
361 |
+ } |
|
362 |
+ return nil |
|
363 |
+ }, |
|
364 |
+} |
|
365 |
+``` |
|
366 |
+ |
|
367 |
+The error can then be caught at the execute function call. |
|
368 |
+ |
|
338 | 369 |
## Working with Flags |
339 | 370 |
|
340 | 371 |
Flags provide modifiers to control how the action command operates. |
... | ... |
@@ -410,6 +419,12 @@ rootCmd.Flags().StringVarP(&Region, "region", "r", "", "AWS region (required)") |
410 | 410 |
rootCmd.MarkFlagRequired("region") |
411 | 411 |
``` |
412 | 412 |
|
413 |
+Or, for persistent flags: |
|
414 |
+```go |
|
415 |
+rootCmd.PersistentFlags().StringVarP(&Region, "region", "r", "", "AWS region (required)") |
|
416 |
+rootCmd.MarkPersistentFlagRequired("region") |
|
417 |
+``` |
|
418 |
+ |
|
413 | 419 |
## Positional and Custom Arguments |
414 | 420 |
|
415 | 421 |
Validation of positional arguments can be specified using the `Args` field |
... | ... |
@@ -740,30 +755,11 @@ Run 'kubectl help' for usage. |
740 | 740 |
|
741 | 741 |
## Generating documentation for your command |
742 | 742 |
|
743 |
-Cobra can generate documentation based on subcommands, flags, etc. in the following formats: |
|
744 |
- |
|
745 |
-- [Markdown](doc/md_docs.md) |
|
746 |
-- [ReStructured Text](doc/rest_docs.md) |
|
747 |
-- [Man Page](doc/man_docs.md) |
|
748 |
- |
|
749 |
-## Generating bash completions |
|
750 |
- |
|
751 |
-Cobra can generate a bash-completion file. If you add more information to your command, these completions can be amazingly powerful and flexible. Read more about it in [Bash Completions](bash_completions.md). |
|
752 |
- |
|
753 |
-## Generating zsh completions |
|
754 |
- |
|
755 |
-Cobra can generate zsh-completion file. Read more about it in |
|
756 |
-[Zsh Completions](zsh_completions.md). |
|
743 |
+Cobra can generate documentation based on subcommands, flags, etc. Read more about it in the [docs generation documentation](doc/README.md). |
|
757 | 744 |
|
758 |
-# Contributing |
|
745 |
+## Generating shell completions |
|
759 | 746 |
|
760 |
-1. Fork it |
|
761 |
-2. Download your fork to your PC (`git clone https://github.com/your_username/cobra && cd cobra`) |
|
762 |
-3. Create your feature branch (`git checkout -b my-new-feature`) |
|
763 |
-4. Make changes and add them (`git add .`) |
|
764 |
-5. Commit your changes (`git commit -m 'Add some feature'`) |
|
765 |
-6. Push to the branch (`git push origin my-new-feature`) |
|
766 |
-7. Create new pull request |
|
747 |
+Cobra can generate a shell-completion file for the following shells: Bash, Zsh, Fish, Powershell. If you add more information to your commands, these completions can be amazingly powerful and flexible. Read more about it in [Shell Completions](shell_completions.md). |
|
767 | 748 |
|
768 | 749 |
# License |
769 | 750 |
|
... | ... |
@@ -62,6 +62,12 @@ __%[1]s_handle_go_custom_completion() |
62 | 62 |
{ |
63 | 63 |
__%[1]s_debug "${FUNCNAME[0]}: cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}" |
64 | 64 |
|
65 |
+ local shellCompDirectiveError=%[3]d |
|
66 |
+ local shellCompDirectiveNoSpace=%[4]d |
|
67 |
+ local shellCompDirectiveNoFileComp=%[5]d |
|
68 |
+ local shellCompDirectiveFilterFileExt=%[6]d |
|
69 |
+ local shellCompDirectiveFilterDirs=%[7]d |
|
70 |
+ |
|
65 | 71 |
local out requestComp lastParam lastChar comp directive args |
66 | 72 |
|
67 | 73 |
# Prepare the command to request completions for the program. |
... | ... |
@@ -95,24 +101,50 @@ __%[1]s_handle_go_custom_completion() |
95 | 95 |
__%[1]s_debug "${FUNCNAME[0]}: the completion directive is: ${directive}" |
96 | 96 |
__%[1]s_debug "${FUNCNAME[0]}: the completions are: ${out[*]}" |
97 | 97 |
|
98 |
- if [ $((directive & %[3]d)) -ne 0 ]; then |
|
98 |
+ if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then |
|
99 | 99 |
# Error code. No completion. |
100 | 100 |
__%[1]s_debug "${FUNCNAME[0]}: received error from custom completion go code" |
101 | 101 |
return |
102 | 102 |
else |
103 |
- if [ $((directive & %[4]d)) -ne 0 ]; then |
|
103 |
+ if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then |
|
104 | 104 |
if [[ $(type -t compopt) = "builtin" ]]; then |
105 | 105 |
__%[1]s_debug "${FUNCNAME[0]}: activating no space" |
106 | 106 |
compopt -o nospace |
107 | 107 |
fi |
108 | 108 |
fi |
109 |
- if [ $((directive & %[5]d)) -ne 0 ]; then |
|
109 |
+ if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then |
|
110 | 110 |
if [[ $(type -t compopt) = "builtin" ]]; then |
111 | 111 |
__%[1]s_debug "${FUNCNAME[0]}: activating no file completion" |
112 | 112 |
compopt +o default |
113 | 113 |
fi |
114 | 114 |
fi |
115 |
+ fi |
|
115 | 116 |
|
117 |
+ if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then |
|
118 |
+ # File extension filtering |
|
119 |
+ local fullFilter filter filteringCmd |
|
120 |
+ # Do not use quotes around the $out variable or else newline |
|
121 |
+ # characters will be kept. |
|
122 |
+ for filter in ${out[*]}; do |
|
123 |
+ fullFilter+="$filter|" |
|
124 |
+ done |
|
125 |
+ |
|
126 |
+ filteringCmd="_filedir $fullFilter" |
|
127 |
+ __%[1]s_debug "File filtering command: $filteringCmd" |
|
128 |
+ $filteringCmd |
|
129 |
+ elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then |
|
130 |
+ # File completion for directories only |
|
131 |
+ local subDir |
|
132 |
+ # Use printf to strip any trailing newline |
|
133 |
+ subdir=$(printf "%%s" "${out[0]}") |
|
134 |
+ if [ -n "$subdir" ]; then |
|
135 |
+ __%[1]s_debug "Listing directories in $subdir" |
|
136 |
+ __%[1]s_handle_subdirs_in_dir_flag "$subdir" |
|
137 |
+ else |
|
138 |
+ __%[1]s_debug "Listing directories in ." |
|
139 |
+ _filedir -d |
|
140 |
+ fi |
|
141 |
+ else |
|
116 | 142 |
while IFS='' read -r comp; do |
117 | 143 |
COMPREPLY+=("$comp") |
118 | 144 |
done < <(compgen -W "${out[*]}" -- "$cur") |
... | ... |
@@ -181,10 +213,9 @@ __%[1]s_handle_reply() |
181 | 181 |
local completions |
182 | 182 |
completions=("${commands[@]}") |
183 | 183 |
if [[ ${#must_have_one_noun[@]} -ne 0 ]]; then |
184 |
- completions=("${must_have_one_noun[@]}") |
|
184 |
+ completions+=("${must_have_one_noun[@]}") |
|
185 | 185 |
elif [[ -n "${has_completion_function}" ]]; then |
186 | 186 |
# if a go completion function is provided, defer to that function |
187 |
- completions=() |
|
188 | 187 |
__%[1]s_handle_go_custom_completion |
189 | 188 |
fi |
190 | 189 |
if [[ ${#must_have_one_flag[@]} -ne 0 ]]; then |
... | ... |
@@ -344,7 +375,9 @@ __%[1]s_handle_word() |
344 | 344 |
__%[1]s_handle_word |
345 | 345 |
} |
346 | 346 |
|
347 |
-`, name, ShellCompNoDescRequestCmd, ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp)) |
|
347 |
+`, name, ShellCompNoDescRequestCmd, |
|
348 |
+ ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, |
|
349 |
+ ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs)) |
|
348 | 350 |
} |
349 | 351 |
|
350 | 352 |
func writePostscript(buf *bytes.Buffer, name string) { |
... | ... |
@@ -390,7 +423,7 @@ fi |
390 | 390 |
func writeCommands(buf *bytes.Buffer, cmd *Command) { |
391 | 391 |
buf.WriteString(" commands=()\n") |
392 | 392 |
for _, c := range cmd.Commands() { |
393 |
- if !c.IsAvailableCommand() || c == cmd.helpCommand { |
|
393 |
+ if !c.IsAvailableCommand() && c != cmd.helpCommand { |
|
394 | 394 |
continue |
395 | 395 |
} |
396 | 396 |
buf.WriteString(fmt.Sprintf(" commands+=(%q)\n", c.Name())) |
... | ... |
@@ -462,12 +495,14 @@ func writeFlag(buf *bytes.Buffer, flag *pflag.Flag, cmd *Command) { |
462 | 462 |
|
463 | 463 |
func writeLocalNonPersistentFlag(buf *bytes.Buffer, flag *pflag.Flag) { |
464 | 464 |
name := flag.Name |
465 |
- format := " local_nonpersistent_flags+=(\"--%s" |
|
465 |
+ format := " local_nonpersistent_flags+=(\"--%[1]s\")\n" |
|
466 | 466 |
if len(flag.NoOptDefVal) == 0 { |
467 |
- format += "=" |
|
467 |
+ format += " local_nonpersistent_flags+=(\"--%[1]s=\")\n" |
|
468 | 468 |
} |
469 |
- format += "\")\n" |
|
470 | 469 |
buf.WriteString(fmt.Sprintf(format, name)) |
470 |
+ if len(flag.Shorthand) > 0 { |
|
471 |
+ buf.WriteString(fmt.Sprintf(" local_nonpersistent_flags+=(\"-%s\")\n", flag.Shorthand)) |
|
472 |
+ } |
|
471 | 473 |
} |
472 | 474 |
|
473 | 475 |
// Setup annotations for go completions for registered flags |
... | ... |
@@ -502,7 +537,9 @@ func writeFlags(buf *bytes.Buffer, cmd *Command) { |
502 | 502 |
if len(flag.Shorthand) > 0 { |
503 | 503 |
writeShortFlag(buf, flag, cmd) |
504 | 504 |
} |
505 |
- if localNonPersistentFlags.Lookup(flag.Name) != nil { |
|
505 |
+ // localNonPersistentFlags are used to stop the completion of subcommands when one is set |
|
506 |
+ // if TraverseChildren is true we should allow to complete subcommands |
|
507 |
+ if localNonPersistentFlags.Lookup(flag.Name) != nil && !cmd.Root().TraverseChildren { |
|
506 | 508 |
writeLocalNonPersistentFlag(buf, flag) |
507 | 509 |
} |
508 | 510 |
}) |
... | ... |
@@ -583,7 +620,7 @@ func writeArgAliases(buf *bytes.Buffer, cmd *Command) { |
583 | 583 |
|
584 | 584 |
func gen(buf *bytes.Buffer, cmd *Command) { |
585 | 585 |
for _, c := range cmd.Commands() { |
586 |
- if !c.IsAvailableCommand() || c == cmd.helpCommand { |
|
586 |
+ if !c.IsAvailableCommand() && c != cmd.helpCommand { |
|
587 | 587 |
continue |
588 | 588 |
} |
589 | 589 |
gen(buf, c) |
... | ... |
@@ -37,6 +37,14 @@ type FParseErrWhitelist flag.ParseErrorsWhitelist |
37 | 37 |
// definition to ensure usability. |
38 | 38 |
type Command struct { |
39 | 39 |
// Use is the one-line usage message. |
40 |
+ // Recommended syntax is as follow: |
|
41 |
+ // [ ] identifies an optional argument. Arguments that are not enclosed in brackets are required. |
|
42 |
+ // ... indicates that you can specify multiple values for the previous argument. |
|
43 |
+ // | indicates mutually exclusive information. You can use the argument to the left of the separator or the |
|
44 |
+ // argument to the right of the separator. You cannot use both arguments in a single use of the command. |
|
45 |
+ // { } delimits a set of mutually exclusive arguments when one of the arguments is required. If the arguments are |
|
46 |
+ // optional, they are enclosed in brackets ([ ]). |
|
47 |
+ // Example: add [-F file | -D dir]... [-f format] profile |
|
40 | 48 |
Use string |
41 | 49 |
|
42 | 50 |
// Aliases is an array of aliases that can be used instead of the first word in Use. |
... | ... |
@@ -359,7 +367,7 @@ func (c *Command) UsageFunc() (f func(*Command) error) { |
359 | 359 |
c.mergePersistentFlags() |
360 | 360 |
err := tmpl(c.OutOrStderr(), c.UsageTemplate(), c) |
361 | 361 |
if err != nil { |
362 |
- c.Println(err) |
|
362 |
+ c.PrintErrln(err) |
|
363 | 363 |
} |
364 | 364 |
return err |
365 | 365 |
} |
... | ... |
@@ -387,7 +395,7 @@ func (c *Command) HelpFunc() func(*Command, []string) { |
387 | 387 |
// See https://github.com/spf13/cobra/issues/1002 |
388 | 388 |
err := tmpl(c.OutOrStdout(), c.HelpTemplate(), c) |
389 | 389 |
if err != nil { |
390 |
- c.Println(err) |
|
390 |
+ c.PrintErrln(err) |
|
391 | 391 |
} |
392 | 392 |
} |
393 | 393 |
} |
... | ... |
@@ -930,8 +938,8 @@ func (c *Command) ExecuteC() (cmd *Command, err error) { |
930 | 930 |
c = cmd |
931 | 931 |
} |
932 | 932 |
if !c.SilenceErrors { |
933 |
- c.Println("Error:", err.Error()) |
|
934 |
- c.Printf("Run '%v --help' for usage.\n", c.CommandPath()) |
|
933 |
+ c.PrintErrln("Error:", err.Error()) |
|
934 |
+ c.PrintErrf("Run '%v --help' for usage.\n", c.CommandPath()) |
|
935 | 935 |
} |
936 | 936 |
return c, err |
937 | 937 |
} |
... | ... |
@@ -959,7 +967,7 @@ func (c *Command) ExecuteC() (cmd *Command, err error) { |
959 | 959 |
// If root command has SilentErrors flagged, |
960 | 960 |
// all subcommands should respect it |
961 | 961 |
if !cmd.SilenceErrors && !c.SilenceErrors { |
962 |
- c.Println("Error:", err.Error()) |
|
962 |
+ c.PrintErrln("Error:", err.Error()) |
|
963 | 963 |
} |
964 | 964 |
|
965 | 965 |
// If root command has SilentUsage flagged, |
... | ... |
@@ -979,6 +987,10 @@ func (c *Command) ValidateArgs(args []string) error { |
979 | 979 |
} |
980 | 980 |
|
981 | 981 |
func (c *Command) validateRequiredFlags() error { |
982 |
+ if c.DisableFlagParsing { |
|
983 |
+ return nil |
|
984 |
+ } |
|
985 |
+ |
|
982 | 986 |
flags := c.Flags() |
983 | 987 |
missingFlagNames := []string{} |
984 | 988 |
flags.VisitAll(func(pflag *flag.Flag) { |
... | ... |
@@ -1052,7 +1064,25 @@ func (c *Command) InitDefaultHelpCmd() { |
1052 | 1052 |
Short: "Help about any command", |
1053 | 1053 |
Long: `Help provides help for any command in the application. |
1054 | 1054 |
Simply type ` + c.Name() + ` help [path to command] for full details.`, |
1055 |
- |
|
1055 |
+ ValidArgsFunction: func(c *Command, args []string, toComplete string) ([]string, ShellCompDirective) { |
|
1056 |
+ var completions []string |
|
1057 |
+ cmd, _, e := c.Root().Find(args) |
|
1058 |
+ if e != nil { |
|
1059 |
+ return nil, ShellCompDirectiveNoFileComp |
|
1060 |
+ } |
|
1061 |
+ if cmd == nil { |
|
1062 |
+ // Root help command. |
|
1063 |
+ cmd = c.Root() |
|
1064 |
+ } |
|
1065 |
+ for _, subCmd := range cmd.Commands() { |
|
1066 |
+ if subCmd.IsAvailableCommand() || subCmd == cmd.helpCommand { |
|
1067 |
+ if strings.HasPrefix(subCmd.Name(), toComplete) { |
|
1068 |
+ completions = append(completions, fmt.Sprintf("%s\t%s", subCmd.Name(), subCmd.Short)) |
|
1069 |
+ } |
|
1070 |
+ } |
|
1071 |
+ } |
|
1072 |
+ return completions, ShellCompDirectiveNoFileComp |
|
1073 |
+ }, |
|
1056 | 1074 |
Run: func(c *Command, args []string) { |
1057 | 1075 |
cmd, _, e := c.Root().Find(args) |
1058 | 1076 |
if cmd == nil || e != nil { |
... | ... |
@@ -1179,12 +1209,12 @@ func (c *Command) PrintErr(i ...interface{}) { |
1179 | 1179 |
|
1180 | 1180 |
// PrintErrln is a convenience method to Println to the defined Err output, fallback to Stderr if not set. |
1181 | 1181 |
func (c *Command) PrintErrln(i ...interface{}) { |
1182 |
- c.Print(fmt.Sprintln(i...)) |
|
1182 |
+ c.PrintErr(fmt.Sprintln(i...)) |
|
1183 | 1183 |
} |
1184 | 1184 |
|
1185 | 1185 |
// PrintErrf is a convenience method to Printf to the defined Err output, fallback to Stderr if not set. |
1186 | 1186 |
func (c *Command) PrintErrf(format string, i ...interface{}) { |
1187 |
- c.Print(fmt.Sprintf(format, i...)) |
|
1187 |
+ c.PrintErr(fmt.Sprintf(format, i...)) |
|
1188 | 1188 |
} |
1189 | 1189 |
|
1190 | 1190 |
// CommandPath returns the full path to this command. |
... | ... |
@@ -1,7 +1,6 @@ |
1 | 1 |
package cobra |
2 | 2 |
|
3 | 3 |
import ( |
4 |
- "errors" |
|
5 | 4 |
"fmt" |
6 | 5 |
"os" |
7 | 6 |
"strings" |
... | ... |
@@ -38,8 +37,29 @@ const ( |
38 | 38 |
// This currently does not work for zsh or bash < 4 |
39 | 39 |
ShellCompDirectiveNoFileComp |
40 | 40 |
|
41 |
+ // ShellCompDirectiveFilterFileExt indicates that the provided completions |
|
42 |
+ // should be used as file extension filters. |
|
43 |
+ // For flags, using Command.MarkFlagFilename() and Command.MarkPersistentFlagFilename() |
|
44 |
+ // is a shortcut to using this directive explicitly. The BashCompFilenameExt |
|
45 |
+ // annotation can also be used to obtain the same behavior for flags. |
|
46 |
+ ShellCompDirectiveFilterFileExt |
|
47 |
+ |
|
48 |
+ // ShellCompDirectiveFilterDirs indicates that only directory names should |
|
49 |
+ // be provided in file completion. To request directory names within another |
|
50 |
+ // directory, the returned completions should specify the directory within |
|
51 |
+ // which to search. The BashCompSubdirsInDir annotation can be used to |
|
52 |
+ // obtain the same behavior but only for flags. |
|
53 |
+ ShellCompDirectiveFilterDirs |
|
54 |
+ |
|
55 |
+ // =========================================================================== |
|
56 |
+ |
|
57 |
+ // All directives using iota should be above this one. |
|
58 |
+ // For internal use. |
|
59 |
+ shellCompDirectiveMaxValue |
|
60 |
+ |
|
41 | 61 |
// ShellCompDirectiveDefault indicates to let the shell perform its default |
42 | 62 |
// behavior after completions have been provided. |
63 |
+ // This one must be last to avoid messing up the iota count. |
|
43 | 64 |
ShellCompDirectiveDefault ShellCompDirective = 0 |
44 | 65 |
) |
45 | 66 |
|
... | ... |
@@ -68,11 +88,17 @@ func (d ShellCompDirective) string() string { |
68 | 68 |
if d&ShellCompDirectiveNoFileComp != 0 { |
69 | 69 |
directives = append(directives, "ShellCompDirectiveNoFileComp") |
70 | 70 |
} |
71 |
+ if d&ShellCompDirectiveFilterFileExt != 0 { |
|
72 |
+ directives = append(directives, "ShellCompDirectiveFilterFileExt") |
|
73 |
+ } |
|
74 |
+ if d&ShellCompDirectiveFilterDirs != 0 { |
|
75 |
+ directives = append(directives, "ShellCompDirectiveFilterDirs") |
|
76 |
+ } |
|
71 | 77 |
if len(directives) == 0 { |
72 | 78 |
directives = append(directives, "ShellCompDirectiveDefault") |
73 | 79 |
} |
74 | 80 |
|
75 |
- if d > ShellCompDirectiveError+ShellCompDirectiveNoSpace+ShellCompDirectiveNoFileComp { |
|
81 |
+ if d >= shellCompDirectiveMaxValue { |
|
76 | 82 |
return fmt.Sprintf("ERROR: unexpected ShellCompDirective value: %d", d) |
77 | 83 |
} |
78 | 84 |
return strings.Join(directives, ", ") |
... | ... |
@@ -105,11 +131,25 @@ func (c *Command) initCompleteCmd(args []string) { |
105 | 105 |
// Remove any description that may be included following a tab character. |
106 | 106 |
comp = strings.Split(comp, "\t")[0] |
107 | 107 |
} |
108 |
+ |
|
109 |
+ // Make sure we only write the first line to the output. |
|
110 |
+ // This is needed if a description contains a linebreak. |
|
111 |
+ // Otherwise the shell scripts will interpret the other lines as new flags |
|
112 |
+ // and could therefore provide a wrong completion. |
|
113 |
+ comp = strings.Split(comp, "\n")[0] |
|
114 |
+ |
|
115 |
+ // Finally trim the completion. This is especially important to get rid |
|
116 |
+ // of a trailing tab when there are no description following it. |
|
117 |
+ // For example, a sub-command without a description should not be completed |
|
118 |
+ // with a tab at the end (or else zsh will show a -- following it |
|
119 |
+ // although there is no description). |
|
120 |
+ comp = strings.TrimSpace(comp) |
|
121 |
+ |
|
108 | 122 |
// Print each possible completion to stdout for the completion script to consume. |
109 | 123 |
fmt.Fprintln(finalCmd.OutOrStdout(), comp) |
110 | 124 |
} |
111 | 125 |
|
112 |
- if directive > ShellCompDirectiveError+ShellCompDirectiveNoSpace+ShellCompDirectiveNoFileComp { |
|
126 |
+ if directive >= shellCompDirectiveMaxValue { |
|
113 | 127 |
directive = ShellCompDirectiveDefault |
114 | 128 |
} |
115 | 129 |
|
... | ... |
@@ -136,90 +176,179 @@ func (c *Command) initCompleteCmd(args []string) { |
136 | 136 |
} |
137 | 137 |
|
138 | 138 |
func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDirective, error) { |
139 |
- var completions []string |
|
140 |
- |
|
141 | 139 |
// The last argument, which is not completely typed by the user, |
142 | 140 |
// should not be part of the list of arguments |
143 | 141 |
toComplete := args[len(args)-1] |
144 | 142 |
trimmedArgs := args[:len(args)-1] |
145 | 143 |
|
144 |
+ var finalCmd *Command |
|
145 |
+ var finalArgs []string |
|
146 |
+ var err error |
|
146 | 147 |
// Find the real command for which completion must be performed |
147 |
- finalCmd, finalArgs, err := c.Root().Find(trimmedArgs) |
|
148 |
+ // check if we need to traverse here to parse local flags on parent commands |
|
149 |
+ if c.Root().TraverseChildren { |
|
150 |
+ finalCmd, finalArgs, err = c.Root().Traverse(trimmedArgs) |
|
151 |
+ } else { |
|
152 |
+ finalCmd, finalArgs, err = c.Root().Find(trimmedArgs) |
|
153 |
+ } |
|
148 | 154 |
if err != nil { |
149 | 155 |
// Unable to find the real command. E.g., <program> someInvalidCmd <TAB> |
150 |
- return c, completions, ShellCompDirectiveDefault, fmt.Errorf("Unable to find a command for arguments: %v", trimmedArgs) |
|
156 |
+ return c, []string{}, ShellCompDirectiveDefault, fmt.Errorf("Unable to find a command for arguments: %v", trimmedArgs) |
|
157 |
+ } |
|
158 |
+ |
|
159 |
+ // Check if we are doing flag value completion before parsing the flags. |
|
160 |
+ // This is important because if we are completing a flag value, we need to also |
|
161 |
+ // remove the flag name argument from the list of finalArgs or else the parsing |
|
162 |
+ // could fail due to an invalid value (incomplete) for the flag. |
|
163 |
+ flag, finalArgs, toComplete, err := checkIfFlagCompletion(finalCmd, finalArgs, toComplete) |
|
164 |
+ if err != nil { |
|
165 |
+ // Error while attempting to parse flags |
|
166 |
+ return finalCmd, []string{}, ShellCompDirectiveDefault, err |
|
167 |
+ } |
|
168 |
+ |
|
169 |
+ // Parse the flags early so we can check if required flags are set |
|
170 |
+ if err = finalCmd.ParseFlags(finalArgs); err != nil { |
|
171 |
+ return finalCmd, []string{}, ShellCompDirectiveDefault, fmt.Errorf("Error while parsing flags from args %v: %s", finalArgs, err.Error()) |
|
172 |
+ } |
|
173 |
+ |
|
174 |
+ if flag != nil { |
|
175 |
+ // Check if we are completing a flag value subject to annotations |
|
176 |
+ if validExts, present := flag.Annotations[BashCompFilenameExt]; present { |
|
177 |
+ if len(validExts) != 0 { |
|
178 |
+ // File completion filtered by extensions |
|
179 |
+ return finalCmd, validExts, ShellCompDirectiveFilterFileExt, nil |
|
180 |
+ } |
|
181 |
+ |
|
182 |
+ // The annotation requests simple file completion. There is no reason to do |
|
183 |
+ // that since it is the default behavior anyway. Let's ignore this annotation |
|
184 |
+ // in case the program also registered a completion function for this flag. |
|
185 |
+ // Even though it is a mistake on the program's side, let's be nice when we can. |
|
186 |
+ } |
|
187 |
+ |
|
188 |
+ if subDir, present := flag.Annotations[BashCompSubdirsInDir]; present { |
|
189 |
+ if len(subDir) == 1 { |
|
190 |
+ // Directory completion from within a directory |
|
191 |
+ return finalCmd, subDir, ShellCompDirectiveFilterDirs, nil |
|
192 |
+ } |
|
193 |
+ // Directory completion |
|
194 |
+ return finalCmd, []string{}, ShellCompDirectiveFilterDirs, nil |
|
195 |
+ } |
|
151 | 196 |
} |
152 | 197 |
|
153 | 198 |
// When doing completion of a flag name, as soon as an argument starts with |
154 | 199 |
// a '-' we know it is a flag. We cannot use isFlagArg() here as it requires |
155 |
- // the flag to be complete |
|
156 |
- if len(toComplete) > 0 && toComplete[0] == '-' && !strings.Contains(toComplete, "=") { |
|
157 |
- // We are completing a flag name |
|
158 |
- finalCmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) { |
|
159 |
- completions = append(completions, getFlagNameCompletions(flag, toComplete)...) |
|
160 |
- }) |
|
161 |
- finalCmd.InheritedFlags().VisitAll(func(flag *pflag.Flag) { |
|
162 |
- completions = append(completions, getFlagNameCompletions(flag, toComplete)...) |
|
163 |
- }) |
|
164 |
- |
|
165 |
- directive := ShellCompDirectiveDefault |
|
166 |
- if len(completions) > 0 { |
|
167 |
- if strings.HasSuffix(completions[0], "=") { |
|
168 |
- directive = ShellCompDirectiveNoSpace |
|
200 |
+ // the flag name to be complete |
|
201 |
+ if flag == nil && len(toComplete) > 0 && toComplete[0] == '-' && !strings.Contains(toComplete, "=") { |
|
202 |
+ var completions []string |
|
203 |
+ |
|
204 |
+ // First check for required flags |
|
205 |
+ completions = completeRequireFlags(finalCmd, toComplete) |
|
206 |
+ |
|
207 |
+ // If we have not found any required flags, only then can we show regular flags |
|
208 |
+ if len(completions) == 0 { |
|
209 |
+ doCompleteFlags := func(flag *pflag.Flag) { |
|
210 |
+ if !flag.Changed || |
|
211 |
+ strings.Contains(flag.Value.Type(), "Slice") || |
|
212 |
+ strings.Contains(flag.Value.Type(), "Array") { |
|
213 |
+ // If the flag is not already present, or if it can be specified multiple times (Array or Slice) |
|
214 |
+ // we suggest it as a completion |
|
215 |
+ completions = append(completions, getFlagNameCompletions(flag, toComplete)...) |
|
216 |
+ } |
|
169 | 217 |
} |
218 |
+ |
|
219 |
+ // We cannot use finalCmd.Flags() because we may not have called ParsedFlags() for commands |
|
220 |
+ // that have set DisableFlagParsing; it is ParseFlags() that merges the inherited and |
|
221 |
+ // non-inherited flags. |
|
222 |
+ finalCmd.InheritedFlags().VisitAll(func(flag *pflag.Flag) { |
|
223 |
+ doCompleteFlags(flag) |
|
224 |
+ }) |
|
225 |
+ finalCmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) { |
|
226 |
+ doCompleteFlags(flag) |
|
227 |
+ }) |
|
228 |
+ } |
|
229 |
+ |
|
230 |
+ directive := ShellCompDirectiveNoFileComp |
|
231 |
+ if len(completions) == 1 && strings.HasSuffix(completions[0], "=") { |
|
232 |
+ // If there is a single completion, the shell usually adds a space |
|
233 |
+ // after the completion. We don't want that if the flag ends with an = |
|
234 |
+ directive = ShellCompDirectiveNoSpace |
|
170 | 235 |
} |
171 | 236 |
return finalCmd, completions, directive, nil |
172 | 237 |
} |
173 | 238 |
|
174 |
- var flag *pflag.Flag |
|
239 |
+ // We only remove the flags from the arguments if DisableFlagParsing is not set. |
|
240 |
+ // This is important for commands which have requested to do their own flag completion. |
|
175 | 241 |
if !finalCmd.DisableFlagParsing { |
176 |
- // We only do flag completion if we are allowed to parse flags |
|
177 |
- // This is important for commands which have requested to do their own flag completion. |
|
178 |
- flag, finalArgs, toComplete, err = checkIfFlagCompletion(finalCmd, finalArgs, toComplete) |
|
179 |
- if err != nil { |
|
180 |
- // Error while attempting to parse flags |
|
181 |
- return finalCmd, completions, ShellCompDirectiveDefault, err |
|
182 |
- } |
|
242 |
+ finalArgs = finalCmd.Flags().Args() |
|
183 | 243 |
} |
184 | 244 |
|
245 |
+ var completions []string |
|
246 |
+ directive := ShellCompDirectiveDefault |
|
185 | 247 |
if flag == nil { |
186 |
- // Complete subcommand names |
|
187 |
- for _, subCmd := range finalCmd.Commands() { |
|
188 |
- if subCmd.IsAvailableCommand() && strings.HasPrefix(subCmd.Name(), toComplete) { |
|
189 |
- completions = append(completions, fmt.Sprintf("%s\t%s", subCmd.Name(), subCmd.Short)) |
|
248 |
+ foundLocalNonPersistentFlag := false |
|
249 |
+ // If TraverseChildren is true on the root command we don't check for |
|
250 |
+ // local flags because we can use a local flag on a parent command |
|
251 |
+ if !finalCmd.Root().TraverseChildren { |
|
252 |
+ // Check if there are any local, non-persistent flags on the command-line |
|
253 |
+ localNonPersistentFlags := finalCmd.LocalNonPersistentFlags() |
|
254 |
+ finalCmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) { |
|
255 |
+ if localNonPersistentFlags.Lookup(flag.Name) != nil && flag.Changed { |
|
256 |
+ foundLocalNonPersistentFlag = true |
|
257 |
+ } |
|
258 |
+ }) |
|
259 |
+ } |
|
260 |
+ |
|
261 |
+ // Complete subcommand names, including the help command |
|
262 |
+ if len(finalArgs) == 0 && !foundLocalNonPersistentFlag { |
|
263 |
+ // We only complete sub-commands if: |
|
264 |
+ // - there are no arguments on the command-line and |
|
265 |
+ // - there are no local, non-peristent flag on the command-line or TraverseChildren is true |
|
266 |
+ for _, subCmd := range finalCmd.Commands() { |
|
267 |
+ if subCmd.IsAvailableCommand() || subCmd == finalCmd.helpCommand { |
|
268 |
+ if strings.HasPrefix(subCmd.Name(), toComplete) { |
|
269 |
+ completions = append(completions, fmt.Sprintf("%s\t%s", subCmd.Name(), subCmd.Short)) |
|
270 |
+ } |
|
271 |
+ directive = ShellCompDirectiveNoFileComp |
|
272 |
+ } |
|
190 | 273 |
} |
191 | 274 |
} |
192 | 275 |
|
276 |
+ // Complete required flags even without the '-' prefix |
|
277 |
+ completions = append(completions, completeRequireFlags(finalCmd, toComplete)...) |
|
278 |
+ |
|
279 |
+ // Always complete ValidArgs, even if we are completing a subcommand name. |
|
280 |
+ // This is for commands that have both subcommands and ValidArgs. |
|
193 | 281 |
if len(finalCmd.ValidArgs) > 0 { |
194 |
- // Always complete ValidArgs, even if we are completing a subcommand name. |
|
195 |
- // This is for commands that have both subcommands and ValidArgs. |
|
196 |
- for _, validArg := range finalCmd.ValidArgs { |
|
197 |
- if strings.HasPrefix(validArg, toComplete) { |
|
198 |
- completions = append(completions, validArg) |
|
282 |
+ if len(finalArgs) == 0 { |
|
283 |
+ // ValidArgs are only for the first argument |
|
284 |
+ for _, validArg := range finalCmd.ValidArgs { |
|
285 |
+ if strings.HasPrefix(validArg, toComplete) { |
|
286 |
+ completions = append(completions, validArg) |
|
287 |
+ } |
|
288 |
+ } |
|
289 |
+ directive = ShellCompDirectiveNoFileComp |
|
290 |
+ |
|
291 |
+ // If no completions were found within commands or ValidArgs, |
|
292 |
+ // see if there are any ArgAliases that should be completed. |
|
293 |
+ if len(completions) == 0 { |
|
294 |
+ for _, argAlias := range finalCmd.ArgAliases { |
|
295 |
+ if strings.HasPrefix(argAlias, toComplete) { |
|
296 |
+ completions = append(completions, argAlias) |
|
297 |
+ } |
|
298 |
+ } |
|
199 | 299 |
} |
200 | 300 |
} |
201 | 301 |
|
202 | 302 |
// If there are ValidArgs specified (even if they don't match), we stop completion. |
203 | 303 |
// Only one of ValidArgs or ValidArgsFunction can be used for a single command. |
204 |
- return finalCmd, completions, ShellCompDirectiveNoFileComp, nil |
|
304 |
+ return finalCmd, completions, directive, nil |
|
205 | 305 |
} |
206 | 306 |
|
207 |
- // Always let the logic continue so as to add any ValidArgsFunction completions, |
|
307 |
+ // Let the logic continue so as to add any ValidArgsFunction completions, |
|
208 | 308 |
// even if we already found sub-commands. |
209 | 309 |
// This is for commands that have subcommands but also specify a ValidArgsFunction. |
210 | 310 |
} |
211 | 311 |
|
212 |
- // Parse the flags and extract the arguments to prepare for calling the completion function |
|
213 |
- if err = finalCmd.ParseFlags(finalArgs); err != nil { |
|
214 |
- return finalCmd, completions, ShellCompDirectiveDefault, fmt.Errorf("Error while parsing flags from args %v: %s", finalArgs, err.Error()) |
|
215 |
- } |
|
216 |
- |
|
217 |
- // We only remove the flags from the arguments if DisableFlagParsing is not set. |
|
218 |
- // This is important for commands which have requested to do their own flag completion. |
|
219 |
- if !finalCmd.DisableFlagParsing { |
|
220 |
- finalArgs = finalCmd.Flags().Args() |
|
221 |
- } |
|
222 |
- |
|
223 | 312 |
// Find the completion function for the flag or command |
224 | 313 |
var completionFn func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) |
225 | 314 |
if flag != nil { |
... | ... |
@@ -227,14 +356,14 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi |
227 | 227 |
} else { |
228 | 228 |
completionFn = finalCmd.ValidArgsFunction |
229 | 229 |
} |
230 |
- if completionFn == nil { |
|
231 |
- // Go custom completion not supported/needed for this flag or command |
|
232 |
- return finalCmd, completions, ShellCompDirectiveDefault, nil |
|
230 |
+ if completionFn != nil { |
|
231 |
+ // Go custom completion defined for this flag or command. |
|
232 |
+ // Call the registered completion function to get the completions. |
|
233 |
+ var comps []string |
|
234 |
+ comps, directive = completionFn(finalCmd, finalArgs, toComplete) |
|
235 |
+ completions = append(completions, comps...) |
|
233 | 236 |
} |
234 | 237 |
|
235 |
- // Call the registered completion function to get the completions |
|
236 |
- comps, directive := completionFn(finalCmd, finalArgs, toComplete) |
|
237 |
- completions = append(completions, comps...) |
|
238 | 238 |
return finalCmd, completions, directive, nil |
239 | 239 |
} |
240 | 240 |
|
... | ... |
@@ -249,11 +378,18 @@ func getFlagNameCompletions(flag *pflag.Flag, toComplete string) []string { |
249 | 249 |
// Flag without the = |
250 | 250 |
completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flag.Usage)) |
251 | 251 |
|
252 |
- if len(flag.NoOptDefVal) == 0 { |
|
253 |
- // Flag requires a value, so it can be suffixed with = |
|
254 |
- flagName += "=" |
|
255 |
- completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flag.Usage)) |
|
256 |
- } |
|
252 |
+ // Why suggest both long forms: --flag and --flag= ? |
|
253 |
+ // This forces the user to *always* have to type either an = or a space after the flag name. |
|
254 |
+ // Let's be nice and avoid making users have to do that. |
|
255 |
+ // Since boolean flags and shortname flags don't show the = form, let's go that route and never show it. |
|
256 |
+ // The = form will still work, we just won't suggest it. |
|
257 |
+ // This also makes the list of suggested flags shorter as we avoid all the = forms. |
|
258 |
+ // |
|
259 |
+ // if len(flag.NoOptDefVal) == 0 { |
|
260 |
+ // // Flag requires a value, so it can be suffixed with = |
|
261 |
+ // flagName += "=" |
|
262 |
+ // completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flag.Usage)) |
|
263 |
+ // } |
|
257 | 264 |
} |
258 | 265 |
|
259 | 266 |
flagName = "-" + flag.Shorthand |
... | ... |
@@ -264,17 +400,54 @@ func getFlagNameCompletions(flag *pflag.Flag, toComplete string) []string { |
264 | 264 |
return completions |
265 | 265 |
} |
266 | 266 |
|
267 |
+func completeRequireFlags(finalCmd *Command, toComplete string) []string { |
|
268 |
+ var completions []string |
|
269 |
+ |
|
270 |
+ doCompleteRequiredFlags := func(flag *pflag.Flag) { |
|
271 |
+ if _, present := flag.Annotations[BashCompOneRequiredFlag]; present { |
|
272 |
+ if !flag.Changed { |
|
273 |
+ // If the flag is not already present, we suggest it as a completion |
|
274 |
+ completions = append(completions, getFlagNameCompletions(flag, toComplete)...) |
|
275 |
+ } |
|
276 |
+ } |
|
277 |
+ } |
|
278 |
+ |
|
279 |
+ // We cannot use finalCmd.Flags() because we may not have called ParsedFlags() for commands |
|
280 |
+ // that have set DisableFlagParsing; it is ParseFlags() that merges the inherited and |
|
281 |
+ // non-inherited flags. |
|
282 |
+ finalCmd.InheritedFlags().VisitAll(func(flag *pflag.Flag) { |
|
283 |
+ doCompleteRequiredFlags(flag) |
|
284 |
+ }) |
|
285 |
+ finalCmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) { |
|
286 |
+ doCompleteRequiredFlags(flag) |
|
287 |
+ }) |
|
288 |
+ |
|
289 |
+ return completions |
|
290 |
+} |
|
291 |
+ |
|
267 | 292 |
func checkIfFlagCompletion(finalCmd *Command, args []string, lastArg string) (*pflag.Flag, []string, string, error) { |
293 |
+ if finalCmd.DisableFlagParsing { |
|
294 |
+ // We only do flag completion if we are allowed to parse flags |
|
295 |
+ // This is important for commands which have requested to do their own flag completion. |
|
296 |
+ return nil, args, lastArg, nil |
|
297 |
+ } |
|
298 |
+ |
|
268 | 299 |
var flagName string |
269 | 300 |
trimmedArgs := args |
270 | 301 |
flagWithEqual := false |
271 |
- if isFlagArg(lastArg) { |
|
302 |
+ |
|
303 |
+ // When doing completion of a flag name, as soon as an argument starts with |
|
304 |
+ // a '-' we know it is a flag. We cannot use isFlagArg() here as that function |
|
305 |
+ // requires the flag name to be complete |
|
306 |
+ if len(lastArg) > 0 && lastArg[0] == '-' { |
|
272 | 307 |
if index := strings.Index(lastArg, "="); index >= 0 { |
308 |
+ // Flag with an = |
|
273 | 309 |
flagName = strings.TrimLeft(lastArg[:index], "-") |
274 | 310 |
lastArg = lastArg[index+1:] |
275 | 311 |
flagWithEqual = true |
276 | 312 |
} else { |
277 |
- return nil, nil, "", errors.New("Unexpected completion request for flag") |
|
313 |
+ // Normal flag completion |
|
314 |
+ return nil, args, lastArg, nil |
|
278 | 315 |
} |
279 | 316 |
} |
280 | 317 |
|
... | ... |
@@ -5,9 +5,15 @@ import ( |
5 | 5 |
"fmt" |
6 | 6 |
"io" |
7 | 7 |
"os" |
8 |
+ "strings" |
|
8 | 9 |
) |
9 | 10 |
|
10 | 11 |
func genFishComp(buf *bytes.Buffer, name string, includeDesc bool) { |
12 |
+ // Variables should not contain a '-' or ':' character |
|
13 |
+ nameForVar := name |
|
14 |
+ nameForVar = strings.Replace(nameForVar, "-", "_", -1) |
|
15 |
+ nameForVar = strings.Replace(nameForVar, ":", "_", -1) |
|
16 |
+ |
|
11 | 17 |
compCmd := ShellCompRequestCmd |
12 | 18 |
if !includeDesc { |
13 | 19 |
compCmd = ShellCompNoDescRequestCmd |
... | ... |
@@ -37,7 +43,13 @@ function __%[1]s_perform_completion |
37 | 37 |
end |
38 | 38 |
__%[1]s_debug "emptyArg: $emptyArg" |
39 | 39 |
|
40 |
- set requestComp "$args[1] %[2]s $args[2..-1] $emptyArg" |
|
40 |
+ if not type -q "$args[1]" |
|
41 |
+ # This can happen when "complete --do-complete %[2]s" is called when running this script. |
|
42 |
+ __%[1]s_debug "Cannot find $args[1]. No completions." |
|
43 |
+ return |
|
44 |
+ end |
|
45 |
+ |
|
46 |
+ set requestComp "$args[1] %[3]s $args[2..-1] $emptyArg" |
|
41 | 47 |
__%[1]s_debug "Calling $requestComp" |
42 | 48 |
|
43 | 49 |
set results (eval $requestComp 2> /dev/null) |
... | ... |
@@ -71,7 +83,8 @@ function __%[1]s_prepare_completions |
71 | 71 |
|
72 | 72 |
# Check if the command-line is already provided. This is useful for testing. |
73 | 73 |
if not set --query __%[1]s_comp_commandLine |
74 |
- set __%[1]s_comp_commandLine (commandline) |
|
74 |
+ # Use the -c flag to allow for completion in the middle of the line |
|
75 |
+ set __%[1]s_comp_commandLine (commandline -c) |
|
75 | 76 |
end |
76 | 77 |
__%[1]s_debug "commandLine is: $__%[1]s_comp_commandLine" |
77 | 78 |
|
... | ... |
@@ -83,7 +96,7 @@ function __%[1]s_prepare_completions |
83 | 83 |
__%[1]s_debug "No completion, probably due to a failure" |
84 | 84 |
# Might as well do file completion, in case it helps |
85 | 85 |
set --global __%[1]s_comp_do_file_comp 1 |
86 |
- return 0 |
|
86 |
+ return 1 |
|
87 | 87 |
end |
88 | 88 |
|
89 | 89 |
set directive (string sub --start 2 $results[-1]) |
... | ... |
@@ -92,20 +105,35 @@ function __%[1]s_prepare_completions |
92 | 92 |
__%[1]s_debug "Completions are: $__%[1]s_comp_results" |
93 | 93 |
__%[1]s_debug "Directive is: $directive" |
94 | 94 |
|
95 |
+ set shellCompDirectiveError %[4]d |
|
96 |
+ set shellCompDirectiveNoSpace %[5]d |
|
97 |
+ set shellCompDirectiveNoFileComp %[6]d |
|
98 |
+ set shellCompDirectiveFilterFileExt %[7]d |
|
99 |
+ set shellCompDirectiveFilterDirs %[8]d |
|
100 |
+ |
|
95 | 101 |
if test -z "$directive" |
96 | 102 |
set directive 0 |
97 | 103 |
end |
98 | 104 |
|
99 |
- set compErr (math (math --scale 0 $directive / %[3]d) %% 2) |
|
105 |
+ set compErr (math (math --scale 0 $directive / $shellCompDirectiveError) %% 2) |
|
100 | 106 |
if test $compErr -eq 1 |
101 | 107 |
__%[1]s_debug "Received error directive: aborting." |
102 | 108 |
# Might as well do file completion, in case it helps |
103 | 109 |
set --global __%[1]s_comp_do_file_comp 1 |
104 |
- return 0 |
|
110 |
+ return 1 |
|
105 | 111 |
end |
106 | 112 |
|
107 |
- set nospace (math (math --scale 0 $directive / %[4]d) %% 2) |
|
108 |
- set nofiles (math (math --scale 0 $directive / %[5]d) %% 2) |
|
113 |
+ set filefilter (math (math --scale 0 $directive / $shellCompDirectiveFilterFileExt) %% 2) |
|
114 |
+ set dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) %% 2) |
|
115 |
+ if test $filefilter -eq 1; or test $dirfilter -eq 1 |
|
116 |
+ __%[1]s_debug "File extension filtering or directory filtering not supported" |
|
117 |
+ # Do full file completion instead |
|
118 |
+ set --global __%[1]s_comp_do_file_comp 1 |
|
119 |
+ return 1 |
|
120 |
+ end |
|
121 |
+ |
|
122 |
+ set nospace (math (math --scale 0 $directive / $shellCompDirectiveNoSpace) %% 2) |
|
123 |
+ set nofiles (math (math --scale 0 $directive / $shellCompDirectiveNoFileComp) %% 2) |
|
109 | 124 |
|
110 | 125 |
__%[1]s_debug "nospace: $nospace, nofiles: $nofiles" |
111 | 126 |
|
... | ... |
@@ -132,24 +160,31 @@ function __%[1]s_prepare_completions |
132 | 132 |
return (not set --query __%[1]s_comp_do_file_comp) |
133 | 133 |
end |
134 | 134 |
|
135 |
-# Remove any pre-existing completions for the program since we will be handling all of them |
|
136 |
-# TODO this cleanup is not sufficient. Fish completions are only loaded once the user triggers |
|
137 |
-# them, so the below deletion will not work as it is run too early. What else can we do? |
|
138 |
-complete -c %[1]s -e |
|
135 |
+# Since Fish completions are only loaded once the user triggers them, we trigger them ourselves |
|
136 |
+# so we can properly delete any completions provided by another script. |
|
137 |
+# The space after the the program name is essential to trigger completion for the program |
|
138 |
+# and not completion of the program name itself. |
|
139 |
+complete --do-complete "%[2]s " > /dev/null 2>&1 |
|
140 |
+# Using '> /dev/null 2>&1' since '&>' is not supported in older versions of fish. |
|
141 |
+ |
|
142 |
+# Remove any pre-existing completions for the program since we will be handling all of them. |
|
143 |
+complete -c %[2]s -e |
|
139 | 144 |
|
140 | 145 |
# The order in which the below two lines are defined is very important so that __%[1]s_prepare_completions |
141 | 146 |
# is called first. It is __%[1]s_prepare_completions that sets up the __%[1]s_comp_do_file_comp variable. |
142 | 147 |
# |
143 | 148 |
# This completion will be run second as complete commands are added FILO. |
144 | 149 |
# It triggers file completion choices when __%[1]s_comp_do_file_comp is set. |
145 |
-complete -c %[1]s -n 'set --query __%[1]s_comp_do_file_comp' |
|
150 |
+complete -c %[2]s -n 'set --query __%[1]s_comp_do_file_comp' |
|
146 | 151 |
|
147 | 152 |
# This completion will be run first as complete commands are added FILO. |
148 |
-# The call to __%[1]s_prepare_completions will setup both __%[1]s_comp_results abd __%[1]s_comp_do_file_comp. |
|
153 |
+# The call to __%[1]s_prepare_completions will setup both __%[1]s_comp_results and __%[1]s_comp_do_file_comp. |
|
149 | 154 |
# It provides the program's completion choices. |
150 |
-complete -c %[1]s -n '__%[1]s_prepare_completions' -f -a '$__%[1]s_comp_results' |
|
155 |
+complete -c %[2]s -n '__%[1]s_prepare_completions' -f -a '$__%[1]s_comp_results' |
|
151 | 156 |
|
152 |
-`, name, compCmd, ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp)) |
|
157 |
+`, nameForVar, name, compCmd, |
|
158 |
+ ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, |
|
159 |
+ ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs)) |
|
153 | 160 |
} |
154 | 161 |
|
155 | 162 |
// GenFishCompletion generates fish completion file and writes to the passed writer. |
... | ... |
@@ -6,7 +6,7 @@ require ( |
6 | 6 |
github.com/cpuguy83/go-md2man/v2 v2.0.0 |
7 | 7 |
github.com/inconshreveable/mousetrap v1.0.0 |
8 | 8 |
github.com/mitchellh/go-homedir v1.1.0 |
9 |
- github.com/spf13/pflag v1.0.3 |
|
10 |
- github.com/spf13/viper v1.4.0 |
|
11 |
- gopkg.in/yaml.v2 v2.2.2 |
|
9 |
+ github.com/spf13/pflag v1.0.5 |
|
10 |
+ github.com/spf13/viper v1.7.0 |
|
11 |
+ gopkg.in/yaml.v2 v2.2.8 |
|
12 | 12 |
) |
... | ... |
@@ -4,82 +4,81 @@ import ( |
4 | 4 |
"github.com/spf13/pflag" |
5 | 5 |
) |
6 | 6 |
|
7 |
-// MarkFlagRequired adds the BashCompOneRequiredFlag annotation to the named flag if it exists, |
|
7 |
+// MarkFlagRequired instructs the various shell completion implementations to |
|
8 |
+// prioritize the named flag when performing completion, |
|
8 | 9 |
// and causes your command to report an error if invoked without the flag. |
9 | 10 |
func (c *Command) MarkFlagRequired(name string) error { |
10 | 11 |
return MarkFlagRequired(c.Flags(), name) |
11 | 12 |
} |
12 | 13 |
|
13 |
-// MarkPersistentFlagRequired adds the BashCompOneRequiredFlag annotation to the named persistent flag if it exists, |
|
14 |
+// MarkPersistentFlagRequired instructs the various shell completion implementations to |
|
15 |
+// prioritize the named persistent flag when performing completion, |
|
14 | 16 |
// and causes your command to report an error if invoked without the flag. |
15 | 17 |
func (c *Command) MarkPersistentFlagRequired(name string) error { |
16 | 18 |
return MarkFlagRequired(c.PersistentFlags(), name) |
17 | 19 |
} |
18 | 20 |
|
19 |
-// MarkFlagRequired adds the BashCompOneRequiredFlag annotation to the named flag if it exists, |
|
21 |
+// MarkFlagRequired instructs the various shell completion implementations to |
|
22 |
+// prioritize the named flag when performing completion, |
|
20 | 23 |
// and causes your command to report an error if invoked without the flag. |
21 | 24 |
func MarkFlagRequired(flags *pflag.FlagSet, name string) error { |
22 | 25 |
return flags.SetAnnotation(name, BashCompOneRequiredFlag, []string{"true"}) |
23 | 26 |
} |
24 | 27 |
|
25 |
-// MarkFlagFilename adds the BashCompFilenameExt annotation to the named flag, if it exists. |
|
26 |
-// Generated bash autocompletion will select filenames for the flag, limiting to named extensions if provided. |
|
28 |
+// MarkFlagFilename instructs the various shell completion implementations to |
|
29 |
+// limit completions for the named flag to the specified file extensions. |
|
27 | 30 |
func (c *Command) MarkFlagFilename(name string, extensions ...string) error { |
28 | 31 |
return MarkFlagFilename(c.Flags(), name, extensions...) |
29 | 32 |
} |
30 | 33 |
|
31 | 34 |
// MarkFlagCustom adds the BashCompCustom annotation to the named flag, if it exists. |
32 |
-// Generated bash autocompletion will call the bash function f for the flag. |
|
35 |
+// The bash completion script will call the bash function f for the flag. |
|
36 |
+// |
|
37 |
+// This will only work for bash completion. |
|
38 |
+// It is recommended to instead use c.RegisterFlagCompletionFunc(...) which allows |
|
39 |
+// to register a Go function which will work across all shells. |
|
33 | 40 |
func (c *Command) MarkFlagCustom(name string, f string) error { |
34 | 41 |
return MarkFlagCustom(c.Flags(), name, f) |
35 | 42 |
} |
36 | 43 |
|
37 | 44 |
// MarkPersistentFlagFilename instructs the various shell completion |
38 |
-// implementations to limit completions for this persistent flag to the |
|
39 |
-// specified extensions (patterns). |
|
40 |
-// |
|
41 |
-// Shell Completion compatibility matrix: bash, zsh |
|
45 |
+// implementations to limit completions for the named persistent flag to the |
|
46 |
+// specified file extensions. |
|
42 | 47 |
func (c *Command) MarkPersistentFlagFilename(name string, extensions ...string) error { |
43 | 48 |
return MarkFlagFilename(c.PersistentFlags(), name, extensions...) |
44 | 49 |
} |
45 | 50 |
|
46 | 51 |
// MarkFlagFilename instructs the various shell completion implementations to |
47 |
-// limit completions for this flag to the specified extensions (patterns). |
|
48 |
-// |
|
49 |
-// Shell Completion compatibility matrix: bash, zsh |
|
52 |
+// limit completions for the named flag to the specified file extensions. |
|
50 | 53 |
func MarkFlagFilename(flags *pflag.FlagSet, name string, extensions ...string) error { |
51 | 54 |
return flags.SetAnnotation(name, BashCompFilenameExt, extensions) |
52 | 55 |
} |
53 | 56 |
|
54 |
-// MarkFlagCustom instructs the various shell completion implementations to |
|
55 |
-// limit completions for this flag to the specified extensions (patterns). |
|
57 |
+// MarkFlagCustom adds the BashCompCustom annotation to the named flag, if it exists. |
|
58 |
+// The bash completion script will call the bash function f for the flag. |
|
56 | 59 |
// |
57 |
-// Shell Completion compatibility matrix: bash, zsh |
|
60 |
+// This will only work for bash completion. |
|
61 |
+// It is recommended to instead use c.RegisterFlagCompletionFunc(...) which allows |
|
62 |
+// to register a Go function which will work across all shells. |
|
58 | 63 |
func MarkFlagCustom(flags *pflag.FlagSet, name string, f string) error { |
59 | 64 |
return flags.SetAnnotation(name, BashCompCustom, []string{f}) |
60 | 65 |
} |
61 | 66 |
|
62 | 67 |
// MarkFlagDirname instructs the various shell completion implementations to |
63 |
-// complete only directories with this named flag. |
|
64 |
-// |
|
65 |
-// Shell Completion compatibility matrix: zsh |
|
68 |
+// limit completions for the named flag to directory names. |
|
66 | 69 |
func (c *Command) MarkFlagDirname(name string) error { |
67 | 70 |
return MarkFlagDirname(c.Flags(), name) |
68 | 71 |
} |
69 | 72 |
|
70 | 73 |
// MarkPersistentFlagDirname instructs the various shell completion |
71 |
-// implementations to complete only directories with this persistent named flag. |
|
72 |
-// |
|
73 |
-// Shell Completion compatibility matrix: zsh |
|
74 |
+// implementations to limit completions for the named persistent flag to |
|
75 |
+// directory names. |
|
74 | 76 |
func (c *Command) MarkPersistentFlagDirname(name string) error { |
75 | 77 |
return MarkFlagDirname(c.PersistentFlags(), name) |
76 | 78 |
} |
77 | 79 |
|
78 | 80 |
// MarkFlagDirname instructs the various shell completion implementations to |
79 |
-// complete only directories with this specified flag. |
|
80 |
-// |
|
81 |
-// Shell Completion compatibility matrix: zsh |
|
81 |
+// limit completions for the named flag to directory names. |
|
82 | 82 |
func MarkFlagDirname(flags *pflag.FlagSet, name string) error { |
83 |
- zshPattern := "-(/)" |
|
84 |
- return flags.SetAnnotation(name, zshCompDirname, []string{zshPattern}) |
|
83 |
+ return flags.SetAnnotation(name, BashCompSubdirsInDir, []string{}) |
|
85 | 84 |
} |
... | ... |
@@ -1,336 +1,240 @@ |
1 | 1 |
package cobra |
2 | 2 |
|
3 | 3 |
import ( |
4 |
- "encoding/json" |
|
4 |
+ "bytes" |
|
5 | 5 |
"fmt" |
6 | 6 |
"io" |
7 | 7 |
"os" |
8 |
- "sort" |
|
9 |
- "strings" |
|
10 |
- "text/template" |
|
11 |
- |
|
12 |
- "github.com/spf13/pflag" |
|
13 |
-) |
|
14 |
- |
|
15 |
-const ( |
|
16 |
- zshCompArgumentAnnotation = "cobra_annotations_zsh_completion_argument_annotation" |
|
17 |
- zshCompArgumentFilenameComp = "cobra_annotations_zsh_completion_argument_file_completion" |
|
18 |
- zshCompArgumentWordComp = "cobra_annotations_zsh_completion_argument_word_completion" |
|
19 |
- zshCompDirname = "cobra_annotations_zsh_dirname" |
|
20 | 8 |
) |
21 | 9 |
|
22 |
-var ( |
|
23 |
- zshCompFuncMap = template.FuncMap{ |
|
24 |
- "genZshFuncName": zshCompGenFuncName, |
|
25 |
- "extractFlags": zshCompExtractFlag, |
|
26 |
- "genFlagEntryForZshArguments": zshCompGenFlagEntryForArguments, |
|
27 |
- "extractArgsCompletions": zshCompExtractArgumentCompletionHintsForRendering, |
|
28 |
- } |
|
29 |
- zshCompletionText = ` |
|
30 |
-{{/* should accept Command (that contains subcommands) as parameter */}} |
|
31 |
-{{define "argumentsC" -}} |
|
32 |
-{{ $cmdPath := genZshFuncName .}} |
|
33 |
-function {{$cmdPath}} { |
|
34 |
- local -a commands |
|
35 |
- |
|
36 |
- _arguments -C \{{- range extractFlags .}} |
|
37 |
- {{genFlagEntryForZshArguments .}} \{{- end}} |
|
38 |
- "1: :->cmnds" \ |
|
39 |
- "*::arg:->args" |
|
40 |
- |
|
41 |
- case $state in |
|
42 |
- cmnds) |
|
43 |
- commands=({{range .Commands}}{{if not .Hidden}} |
|
44 |
- "{{.Name}}:{{.Short}}"{{end}}{{end}} |
|
45 |
- ) |
|
46 |
- _describe "command" commands |
|
47 |
- ;; |
|
48 |
- esac |
|
49 |
- |
|
50 |
- case "$words[1]" in {{- range .Commands}}{{if not .Hidden}} |
|
51 |
- {{.Name}}) |
|
52 |
- {{$cmdPath}}_{{.Name}} |
|
53 |
- ;;{{end}}{{end}} |
|
54 |
- esac |
|
55 |
-} |
|
56 |
-{{range .Commands}}{{if not .Hidden}} |
|
57 |
-{{template "selectCmdTemplate" .}} |
|
58 |
-{{- end}}{{end}} |
|
59 |
-{{- end}} |
|
60 |
- |
|
61 |
-{{/* should accept Command without subcommands as parameter */}} |
|
62 |
-{{define "arguments" -}} |
|
63 |
-function {{genZshFuncName .}} { |
|
64 |
-{{" _arguments"}}{{range extractFlags .}} \ |
|
65 |
- {{genFlagEntryForZshArguments . -}} |
|
66 |
-{{end}}{{range extractArgsCompletions .}} \ |
|
67 |
- {{.}}{{end}} |
|
68 |
-} |
|
69 |
-{{end}} |
|
70 |
- |
|
71 |
-{{/* dispatcher for commands with or without subcommands */}} |
|
72 |
-{{define "selectCmdTemplate" -}} |
|
73 |
-{{if .Hidden}}{{/* ignore hidden*/}}{{else -}} |
|
74 |
-{{if .Commands}}{{template "argumentsC" .}}{{else}}{{template "arguments" .}}{{end}} |
|
75 |
-{{- end}} |
|
76 |
-{{- end}} |
|
77 |
- |
|
78 |
-{{/* template entry point */}} |
|
79 |
-{{define "Main" -}} |
|
80 |
-#compdef _{{.Name}} {{.Name}} |
|
81 |
- |
|
82 |
-{{template "selectCmdTemplate" .}} |
|
83 |
-{{end}} |
|
84 |
-` |
|
85 |
-) |
|
86 |
- |
|
87 |
-// zshCompArgsAnnotation is used to encode/decode zsh completion for |
|
88 |
-// arguments to/from Command.Annotations. |
|
89 |
-type zshCompArgsAnnotation map[int]zshCompArgHint |
|
90 |
- |
|
91 |
-type zshCompArgHint struct { |
|
92 |
- // Indicates the type of the completion to use. One of: |
|
93 |
- // zshCompArgumentFilenameComp or zshCompArgumentWordComp |
|
94 |
- Tipe string `json:"type"` |
|
95 |
- |
|
96 |
- // A value for the type above (globs for file completion or words) |
|
97 |
- Options []string `json:"options"` |
|
98 |
-} |
|
99 |
- |
|
100 |
-// GenZshCompletionFile generates zsh completion file. |
|
10 |
+// GenZshCompletionFile generates zsh completion file including descriptions. |
|
101 | 11 |
func (c *Command) GenZshCompletionFile(filename string) error { |
102 |
- outFile, err := os.Create(filename) |
|
103 |
- if err != nil { |
|
104 |
- return err |
|
105 |
- } |
|
106 |
- defer outFile.Close() |
|
107 |
- |
|
108 |
- return c.GenZshCompletion(outFile) |
|
12 |
+ return c.genZshCompletionFile(filename, true) |
|
109 | 13 |
} |
110 | 14 |
|
111 |
-// GenZshCompletion generates a zsh completion file and writes to the passed |
|
112 |
-// writer. The completion always run on the root command regardless of the |
|
113 |
-// command it was called from. |
|
15 |
+// GenZshCompletion generates zsh completion file including descriptions |
|
16 |
+// and writes it to the passed writer. |
|
114 | 17 |
func (c *Command) GenZshCompletion(w io.Writer) error { |
115 |
- tmpl, err := template.New("Main").Funcs(zshCompFuncMap).Parse(zshCompletionText) |
|
116 |
- if err != nil { |
|
117 |
- return fmt.Errorf("error creating zsh completion template: %v", err) |
|
118 |
- } |
|
119 |
- return tmpl.Execute(w, c.Root()) |
|
120 |
-} |
|
121 |
- |
|
122 |
-// MarkZshCompPositionalArgumentFile marks the specified argument (first |
|
123 |
-// argument is 1) as completed by file selection. patterns (e.g. "*.txt") are |
|
124 |
-// optional - if not provided the completion will search for all files. |
|
125 |
-func (c *Command) MarkZshCompPositionalArgumentFile(argPosition int, patterns ...string) error { |
|
126 |
- if argPosition < 1 { |
|
127 |
- return fmt.Errorf("Invalid argument position (%d)", argPosition) |
|
128 |
- } |
|
129 |
- annotation, err := c.zshCompGetArgsAnnotations() |
|
130 |
- if err != nil { |
|
131 |
- return err |
|
132 |
- } |
|
133 |
- if c.zshcompArgsAnnotationnIsDuplicatePosition(annotation, argPosition) { |
|
134 |
- return fmt.Errorf("Duplicate annotation for positional argument at index %d", argPosition) |
|
135 |
- } |
|
136 |
- annotation[argPosition] = zshCompArgHint{ |
|
137 |
- Tipe: zshCompArgumentFilenameComp, |
|
138 |
- Options: patterns, |
|
139 |
- } |
|
140 |
- return c.zshCompSetArgsAnnotations(annotation) |
|
141 |
-} |
|
142 |
- |
|
143 |
-// MarkZshCompPositionalArgumentWords marks the specified positional argument |
|
144 |
-// (first argument is 1) as completed by the provided words. At east one word |
|
145 |
-// must be provided, spaces within words will be offered completion with |
|
146 |
-// "word\ word". |
|
147 |
-func (c *Command) MarkZshCompPositionalArgumentWords(argPosition int, words ...string) error { |
|
148 |
- if argPosition < 1 { |
|
149 |
- return fmt.Errorf("Invalid argument position (%d)", argPosition) |
|
150 |
- } |
|
151 |
- if len(words) == 0 { |
|
152 |
- return fmt.Errorf("Trying to set empty word list for positional argument %d", argPosition) |
|
153 |
- } |
|
154 |
- annotation, err := c.zshCompGetArgsAnnotations() |
|
155 |
- if err != nil { |
|
156 |
- return err |
|
157 |
- } |
|
158 |
- if c.zshcompArgsAnnotationnIsDuplicatePosition(annotation, argPosition) { |
|
159 |
- return fmt.Errorf("Duplicate annotation for positional argument at index %d", argPosition) |
|
160 |
- } |
|
161 |
- annotation[argPosition] = zshCompArgHint{ |
|
162 |
- Tipe: zshCompArgumentWordComp, |
|
163 |
- Options: words, |
|
164 |
- } |
|
165 |
- return c.zshCompSetArgsAnnotations(annotation) |
|
166 |
-} |
|
167 |
- |
|
168 |
-func zshCompExtractArgumentCompletionHintsForRendering(c *Command) ([]string, error) { |
|
169 |
- var result []string |
|
170 |
- annotation, err := c.zshCompGetArgsAnnotations() |
|
171 |
- if err != nil { |
|
172 |
- return nil, err |
|
173 |
- } |
|
174 |
- for k, v := range annotation { |
|
175 |
- s, err := zshCompRenderZshCompArgHint(k, v) |
|
176 |
- if err != nil { |
|
177 |
- return nil, err |
|
178 |
- } |
|
179 |
- result = append(result, s) |
|
180 |
- } |
|
181 |
- if len(c.ValidArgs) > 0 { |
|
182 |
- if _, positionOneExists := annotation[1]; !positionOneExists { |
|
183 |
- s, err := zshCompRenderZshCompArgHint(1, zshCompArgHint{ |
|
184 |
- Tipe: zshCompArgumentWordComp, |
|
185 |
- Options: c.ValidArgs, |
|
186 |
- }) |
|
187 |
- if err != nil { |
|
188 |
- return nil, err |
|
189 |
- } |
|
190 |
- result = append(result, s) |
|
191 |
- } |
|
192 |
- } |
|
193 |
- sort.Strings(result) |
|
194 |
- return result, nil |
|
18 |
+ return c.genZshCompletion(w, true) |
|
195 | 19 |
} |
196 | 20 |
|
197 |
-func zshCompRenderZshCompArgHint(i int, z zshCompArgHint) (string, error) { |
|
198 |
- switch t := z.Tipe; t { |
|
199 |
- case zshCompArgumentFilenameComp: |
|
200 |
- var globs []string |
|
201 |
- for _, g := range z.Options { |
|
202 |
- globs = append(globs, fmt.Sprintf(`-g "%s"`, g)) |
|
203 |
- } |
|
204 |
- return fmt.Sprintf(`'%d: :_files %s'`, i, strings.Join(globs, " ")), nil |
|
205 |
- case zshCompArgumentWordComp: |
|
206 |
- var words []string |
|
207 |
- for _, w := range z.Options { |
|
208 |
- words = append(words, fmt.Sprintf("%q", w)) |
|
209 |
- } |
|
210 |
- return fmt.Sprintf(`'%d: :(%s)'`, i, strings.Join(words, " ")), nil |
|
211 |
- default: |
|
212 |
- return "", fmt.Errorf("Invalid zsh argument completion annotation: %s", t) |
|
213 |
- } |
|
21 |
+// GenZshCompletionFileNoDesc generates zsh completion file without descriptions. |
|
22 |
+func (c *Command) GenZshCompletionFileNoDesc(filename string) error { |
|
23 |
+ return c.genZshCompletionFile(filename, false) |
|
214 | 24 |
} |
215 | 25 |
|
216 |
-func (c *Command) zshcompArgsAnnotationnIsDuplicatePosition(annotation zshCompArgsAnnotation, position int) bool { |
|
217 |
- _, dup := annotation[position] |
|
218 |
- return dup |
|
26 |
+// GenZshCompletionNoDesc generates zsh completion file without descriptions |
|
27 |
+// and writes it to the passed writer. |
|
28 |
+func (c *Command) GenZshCompletionNoDesc(w io.Writer) error { |
|
29 |
+ return c.genZshCompletion(w, false) |
|
219 | 30 |
} |
220 | 31 |
|
221 |
-func (c *Command) zshCompGetArgsAnnotations() (zshCompArgsAnnotation, error) { |
|
222 |
- annotation := make(zshCompArgsAnnotation) |
|
223 |
- annotationString, ok := c.Annotations[zshCompArgumentAnnotation] |
|
224 |
- if !ok { |
|
225 |
- return annotation, nil |
|
226 |
- } |
|
227 |
- err := json.Unmarshal([]byte(annotationString), &annotation) |
|
228 |
- if err != nil { |
|
229 |
- return annotation, fmt.Errorf("Error unmarshaling zsh argument annotation: %v", err) |
|
230 |
- } |
|
231 |
- return annotation, nil |
|
232 |
-} |
|
233 |
- |
|
234 |
-func (c *Command) zshCompSetArgsAnnotations(annotation zshCompArgsAnnotation) error { |
|
235 |
- jsn, err := json.Marshal(annotation) |
|
236 |
- if err != nil { |
|
237 |
- return fmt.Errorf("Error marshaling zsh argument annotation: %v", err) |
|
238 |
- } |
|
239 |
- if c.Annotations == nil { |
|
240 |
- c.Annotations = make(map[string]string) |
|
241 |
- } |
|
242 |
- c.Annotations[zshCompArgumentAnnotation] = string(jsn) |
|
32 |
+// MarkZshCompPositionalArgumentFile only worked for zsh and its behavior was |
|
33 |
+// not consistent with Bash completion. It has therefore been disabled. |
|
34 |
+// Instead, when no other completion is specified, file completion is done by |
|
35 |
+// default for every argument. One can disable file completion on a per-argument |
|
36 |
+// basis by using ValidArgsFunction and ShellCompDirectiveNoFileComp. |
|
37 |
+// To achieve file extension filtering, one can use ValidArgsFunction and |
|
38 |
+// ShellCompDirectiveFilterFileExt. |
|
39 |
+// |
|
40 |
+// Deprecated |
|
41 |
+func (c *Command) MarkZshCompPositionalArgumentFile(argPosition int, patterns ...string) error { |
|
243 | 42 |
return nil |
244 | 43 |
} |
245 | 44 |
|
246 |
-func zshCompGenFuncName(c *Command) string { |
|
247 |
- if c.HasParent() { |
|
248 |
- return zshCompGenFuncName(c.Parent()) + "_" + c.Name() |
|
249 |
- } |
|
250 |
- return "_" + c.Name() |
|
251 |
-} |
|
252 |
- |
|
253 |
-func zshCompExtractFlag(c *Command) []*pflag.Flag { |
|
254 |
- var flags []*pflag.Flag |
|
255 |
- c.LocalFlags().VisitAll(func(f *pflag.Flag) { |
|
256 |
- if !f.Hidden { |
|
257 |
- flags = append(flags, f) |
|
258 |
- } |
|
259 |
- }) |
|
260 |
- c.InheritedFlags().VisitAll(func(f *pflag.Flag) { |
|
261 |
- if !f.Hidden { |
|
262 |
- flags = append(flags, f) |
|
263 |
- } |
|
264 |
- }) |
|
265 |
- return flags |
|
266 |
-} |
|
267 |
- |
|
268 |
-// zshCompGenFlagEntryForArguments returns an entry that matches _arguments |
|
269 |
-// zsh-completion parameters. It's too complicated to generate in a template. |
|
270 |
-func zshCompGenFlagEntryForArguments(f *pflag.Flag) string { |
|
271 |
- if f.Name == "" || f.Shorthand == "" { |
|
272 |
- return zshCompGenFlagEntryForSingleOptionFlag(f) |
|
273 |
- } |
|
274 |
- return zshCompGenFlagEntryForMultiOptionFlag(f) |
|
275 |
-} |
|
276 |
- |
|
277 |
-func zshCompGenFlagEntryForSingleOptionFlag(f *pflag.Flag) string { |
|
278 |
- var option, multiMark, extras string |
|
279 |
- |
|
280 |
- if zshCompFlagCouldBeSpecifiedMoreThenOnce(f) { |
|
281 |
- multiMark = "*" |
|
282 |
- } |
|
283 |
- |
|
284 |
- option = "--" + f.Name |
|
285 |
- if option == "--" { |
|
286 |
- option = "-" + f.Shorthand |
|
287 |
- } |
|
288 |
- extras = zshCompGenFlagEntryExtras(f) |
|
289 |
- |
|
290 |
- return fmt.Sprintf(`'%s%s[%s]%s'`, multiMark, option, zshCompQuoteFlagDescription(f.Usage), extras) |
|
291 |
-} |
|
292 |
- |
|
293 |
-func zshCompGenFlagEntryForMultiOptionFlag(f *pflag.Flag) string { |
|
294 |
- var options, parenMultiMark, curlyMultiMark, extras string |
|
295 |
- |
|
296 |
- if zshCompFlagCouldBeSpecifiedMoreThenOnce(f) { |
|
297 |
- parenMultiMark = "*" |
|
298 |
- curlyMultiMark = "\\*" |
|
299 |
- } |
|
300 |
- |
|
301 |
- options = fmt.Sprintf(`'(%s-%s %s--%s)'{%s-%s,%s--%s}`, |
|
302 |
- parenMultiMark, f.Shorthand, parenMultiMark, f.Name, curlyMultiMark, f.Shorthand, curlyMultiMark, f.Name) |
|
303 |
- extras = zshCompGenFlagEntryExtras(f) |
|
304 |
- |
|
305 |
- return fmt.Sprintf(`%s'[%s]%s'`, options, zshCompQuoteFlagDescription(f.Usage), extras) |
|
45 |
+// MarkZshCompPositionalArgumentWords only worked for zsh. It has therefore |
|
46 |
+// been disabled. |
|
47 |
+// To achieve the same behavior across all shells, one can use |
|
48 |
+// ValidArgs (for the first argument only) or ValidArgsFunction for |
|
49 |
+// any argument (can include the first one also). |
|
50 |
+// |
|
51 |
+// Deprecated |
|
52 |
+func (c *Command) MarkZshCompPositionalArgumentWords(argPosition int, words ...string) error { |
|
53 |
+ return nil |
|
306 | 54 |
} |
307 | 55 |
|
308 |
-func zshCompGenFlagEntryExtras(f *pflag.Flag) string { |
|
309 |
- if f.NoOptDefVal != "" { |
|
310 |
- return "" |
|
311 |
- } |
|
312 |
- |
|
313 |
- extras := ":" // allow options for flag (even without assistance) |
|
314 |
- for key, values := range f.Annotations { |
|
315 |
- switch key { |
|
316 |
- case zshCompDirname: |
|
317 |
- extras = fmt.Sprintf(":filename:_files -g %q", values[0]) |
|
318 |
- case BashCompFilenameExt: |
|
319 |
- extras = ":filename:_files" |
|
320 |
- for _, pattern := range values { |
|
321 |
- extras = extras + fmt.Sprintf(` -g "%s"`, pattern) |
|
322 |
- } |
|
323 |
- } |
|
56 |
+func (c *Command) genZshCompletionFile(filename string, includeDesc bool) error { |
|
57 |
+ outFile, err := os.Create(filename) |
|
58 |
+ if err != nil { |
|
59 |
+ return err |
|
324 | 60 |
} |
61 |
+ defer outFile.Close() |
|
325 | 62 |
|
326 |
- return extras |
|
327 |
-} |
|
328 |
- |
|
329 |
-func zshCompFlagCouldBeSpecifiedMoreThenOnce(f *pflag.Flag) bool { |
|
330 |
- return strings.Contains(f.Value.Type(), "Slice") || |
|
331 |
- strings.Contains(f.Value.Type(), "Array") |
|
332 |
-} |
|
333 |
- |
|
334 |
-func zshCompQuoteFlagDescription(s string) string { |
|
335 |
- return strings.Replace(s, "'", `'\''`, -1) |
|
63 |
+ return c.genZshCompletion(outFile, includeDesc) |
|
64 |
+} |
|
65 |
+ |
|
66 |
+func (c *Command) genZshCompletion(w io.Writer, includeDesc bool) error { |
|
67 |
+ buf := new(bytes.Buffer) |
|
68 |
+ genZshComp(buf, c.Name(), includeDesc) |
|
69 |
+ _, err := buf.WriteTo(w) |
|
70 |
+ return err |
|
71 |
+} |
|
72 |
+ |
|
73 |
+func genZshComp(buf *bytes.Buffer, name string, includeDesc bool) { |
|
74 |
+ compCmd := ShellCompRequestCmd |
|
75 |
+ if !includeDesc { |
|
76 |
+ compCmd = ShellCompNoDescRequestCmd |
|
77 |
+ } |
|
78 |
+ buf.WriteString(fmt.Sprintf(`#compdef _%[1]s %[1]s |
|
79 |
+ |
|
80 |
+# zsh completion for %-36[1]s -*- shell-script -*- |
|
81 |
+ |
|
82 |
+__%[1]s_debug() |
|
83 |
+{ |
|
84 |
+ local file="$BASH_COMP_DEBUG_FILE" |
|
85 |
+ if [[ -n ${file} ]]; then |
|
86 |
+ echo "$*" >> "${file}" |
|
87 |
+ fi |
|
88 |
+} |
|
89 |
+ |
|
90 |
+_%[1]s() |
|
91 |
+{ |
|
92 |
+ local shellCompDirectiveError=%[3]d |
|
93 |
+ local shellCompDirectiveNoSpace=%[4]d |
|
94 |
+ local shellCompDirectiveNoFileComp=%[5]d |
|
95 |
+ local shellCompDirectiveFilterFileExt=%[6]d |
|
96 |
+ local shellCompDirectiveFilterDirs=%[7]d |
|
97 |
+ |
|
98 |
+ local lastParam lastChar flagPrefix requestComp out directive compCount comp lastComp |
|
99 |
+ local -a completions |
|
100 |
+ |
|
101 |
+ __%[1]s_debug "\n========= starting completion logic ==========" |
|
102 |
+ __%[1]s_debug "CURRENT: ${CURRENT}, words[*]: ${words[*]}" |
|
103 |
+ |
|
104 |
+ # The user could have moved the cursor backwards on the command-line. |
|
105 |
+ # We need to trigger completion from the $CURRENT location, so we need |
|
106 |
+ # to truncate the command-line ($words) up to the $CURRENT location. |
|
107 |
+ # (We cannot use $CURSOR as its value does not work when a command is an alias.) |
|
108 |
+ words=("${=words[1,CURRENT]}") |
|
109 |
+ __%[1]s_debug "Truncated words[*]: ${words[*]}," |
|
110 |
+ |
|
111 |
+ lastParam=${words[-1]} |
|
112 |
+ lastChar=${lastParam[-1]} |
|
113 |
+ __%[1]s_debug "lastParam: ${lastParam}, lastChar: ${lastChar}" |
|
114 |
+ |
|
115 |
+ # For zsh, when completing a flag with an = (e.g., %[1]s -n=<TAB>) |
|
116 |
+ # completions must be prefixed with the flag |
|
117 |
+ setopt local_options BASH_REMATCH |
|
118 |
+ if [[ "${lastParam}" =~ '-.*=' ]]; then |
|
119 |
+ # We are dealing with a flag with an = |
|
120 |
+ flagPrefix="-P ${BASH_REMATCH}" |
|
121 |
+ fi |
|
122 |
+ |
|
123 |
+ # Prepare the command to obtain completions |
|
124 |
+ requestComp="${words[1]} %[2]s ${words[2,-1]}" |
|
125 |
+ if [ "${lastChar}" = "" ]; then |
|
126 |
+ # If the last parameter is complete (there is a space following it) |
|
127 |
+ # We add an extra empty parameter so we can indicate this to the go completion code. |
|
128 |
+ __%[1]s_debug "Adding extra empty parameter" |
|
129 |
+ requestComp="${requestComp} \"\"" |
|
130 |
+ fi |
|
131 |
+ |
|
132 |
+ __%[1]s_debug "About to call: eval ${requestComp}" |
|
133 |
+ |
|
134 |
+ # Use eval to handle any environment variables and such |
|
135 |
+ out=$(eval ${requestComp} 2>/dev/null) |
|
136 |
+ __%[1]s_debug "completion output: ${out}" |
|
137 |
+ |
|
138 |
+ # Extract the directive integer following a : from the last line |
|
139 |
+ local lastLine |
|
140 |
+ while IFS='\n' read -r line; do |
|
141 |
+ lastLine=${line} |
|
142 |
+ done < <(printf "%%s\n" "${out[@]}") |
|
143 |
+ __%[1]s_debug "last line: ${lastLine}" |
|
144 |
+ |
|
145 |
+ if [ "${lastLine[1]}" = : ]; then |
|
146 |
+ directive=${lastLine[2,-1]} |
|
147 |
+ # Remove the directive including the : and the newline |
|
148 |
+ local suffix |
|
149 |
+ (( suffix=${#lastLine}+2)) |
|
150 |
+ out=${out[1,-$suffix]} |
|
151 |
+ else |
|
152 |
+ # There is no directive specified. Leave $out as is. |
|
153 |
+ __%[1]s_debug "No directive found. Setting do default" |
|
154 |
+ directive=0 |
|
155 |
+ fi |
|
156 |
+ |
|
157 |
+ __%[1]s_debug "directive: ${directive}" |
|
158 |
+ __%[1]s_debug "completions: ${out}" |
|
159 |
+ __%[1]s_debug "flagPrefix: ${flagPrefix}" |
|
160 |
+ |
|
161 |
+ if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then |
|
162 |
+ __%[1]s_debug "Completion received error. Ignoring completions." |
|
163 |
+ return |
|
164 |
+ fi |
|
165 |
+ |
|
166 |
+ compCount=0 |
|
167 |
+ while IFS='\n' read -r comp; do |
|
168 |
+ if [ -n "$comp" ]; then |
|
169 |
+ # If requested, completions are returned with a description. |
|
170 |
+ # The description is preceded by a TAB character. |
|
171 |
+ # For zsh's _describe, we need to use a : instead of a TAB. |
|
172 |
+ # We first need to escape any : as part of the completion itself. |
|
173 |
+ comp=${comp//:/\\:} |
|
174 |
+ |
|
175 |
+ local tab=$(printf '\t') |
|
176 |
+ comp=${comp//$tab/:} |
|
177 |
+ |
|
178 |
+ ((compCount++)) |
|
179 |
+ __%[1]s_debug "Adding completion: ${comp}" |
|
180 |
+ completions+=${comp} |
|
181 |
+ lastComp=$comp |
|
182 |
+ fi |
|
183 |
+ done < <(printf "%%s\n" "${out[@]}") |
|
184 |
+ |
|
185 |
+ if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then |
|
186 |
+ # File extension filtering |
|
187 |
+ local filteringCmd |
|
188 |
+ filteringCmd='_files' |
|
189 |
+ for filter in ${completions[@]}; do |
|
190 |
+ if [ ${filter[1]} != '*' ]; then |
|
191 |
+ # zsh requires a glob pattern to do file filtering |
|
192 |
+ filter="\*.$filter" |
|
193 |
+ fi |
|
194 |
+ filteringCmd+=" -g $filter" |
|
195 |
+ done |
|
196 |
+ filteringCmd+=" ${flagPrefix}" |
|
197 |
+ |
|
198 |
+ __%[1]s_debug "File filtering command: $filteringCmd" |
|
199 |
+ _arguments '*:filename:'"$filteringCmd" |
|
200 |
+ elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then |
|
201 |
+ # File completion for directories only |
|
202 |
+ local subDir |
|
203 |
+ subdir="${completions[1]}" |
|
204 |
+ if [ -n "$subdir" ]; then |
|
205 |
+ __%[1]s_debug "Listing directories in $subdir" |
|
206 |
+ pushd "${subdir}" >/dev/null 2>&1 |
|
207 |
+ else |
|
208 |
+ __%[1]s_debug "Listing directories in ." |
|
209 |
+ fi |
|
210 |
+ |
|
211 |
+ _arguments '*:dirname:_files -/'" ${flagPrefix}" |
|
212 |
+ if [ -n "$subdir" ]; then |
|
213 |
+ popd >/dev/null 2>&1 |
|
214 |
+ fi |
|
215 |
+ elif [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ] && [ ${compCount} -eq 1 ]; then |
|
216 |
+ __%[1]s_debug "Activating nospace." |
|
217 |
+ # We can use compadd here as there is no description when |
|
218 |
+ # there is only one completion. |
|
219 |
+ compadd -S '' "${lastComp}" |
|
220 |
+ elif [ ${compCount} -eq 0 ]; then |
|
221 |
+ if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then |
|
222 |
+ __%[1]s_debug "deactivating file completion" |
|
223 |
+ else |
|
224 |
+ # Perform file completion |
|
225 |
+ __%[1]s_debug "activating file completion" |
|
226 |
+ _arguments '*:filename:_files'" ${flagPrefix}" |
|
227 |
+ fi |
|
228 |
+ else |
|
229 |
+ _describe "completions" completions $(echo $flagPrefix) |
|
230 |
+ fi |
|
231 |
+} |
|
232 |
+ |
|
233 |
+# don't run the completion function when being source-ed or eval-ed |
|
234 |
+if [ "$funcstack[1]" = "_%[1]s" ]; then |
|
235 |
+ _%[1]s |
|
236 |
+fi |
|
237 |
+`, name, compCmd, |
|
238 |
+ ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, |
|
239 |
+ ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs)) |
|
336 | 240 |
} |