author | Mikael Berthe <mikael@lilotux.net> |
Sun, 16 Feb 2020 18:54:01 +0100 | |
changeset 251 | 1c52a0eeb952 |
parent 242 | 2a9ec03fe5a1 |
child 256 | 6d9efbef00a9 |
permissions | -rw-r--r-- |
242
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
1 |
package cobra |
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
2 |
|
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
3 |
import ( |
251 | 4 |
"encoding/json" |
242
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
5 |
"fmt" |
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
6 |
"io" |
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
7 |
"os" |
251 | 8 |
"sort" |
242
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
9 |
"strings" |
251 | 10 |
"text/template" |
11 |
||
12 |
"github.com/spf13/pflag" |
|
242
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
13 |
) |
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
14 |
|
251 | 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 |
) |
|
21 |
||
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 |
||
242
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
100 |
// GenZshCompletionFile generates zsh completion file. |
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
101 |
func (c *Command) GenZshCompletionFile(filename string) error { |
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
102 |
outFile, err := os.Create(filename) |
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
103 |
if err != nil { |
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
104 |
return err |
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
105 |
} |
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
106 |
defer outFile.Close() |
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
107 |
|
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
108 |
return c.GenZshCompletion(outFile) |
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
109 |
} |
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
110 |
|
251 | 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. |
|
242
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
114 |
func (c *Command) GenZshCompletion(w io.Writer) error { |
251 | 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()) |
|
242
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
120 |
} |
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
121 |
|
251 | 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) |
|
242
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
141 |
} |
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
142 |
|
251 | 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, |
|
242
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
164 |
} |
251 | 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) |
|
242
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
191 |
} |
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
192 |
} |
251 | 193 |
sort.Strings(result) |
194 |
return result, nil |
|
242
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
195 |
} |
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
196 |
|
251 | 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) |
|
242
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
213 |
} |
251 | 214 |
} |
215 |
||
216 |
func (c *Command) zshcompArgsAnnotationnIsDuplicatePosition(annotation zshCompArgsAnnotation, position int) bool { |
|
217 |
_, dup := annotation[position] |
|
218 |
return dup |
|
242
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
219 |
} |
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
220 |
|
251 | 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) |
|
243 |
return nil |
|
244 |
} |
|
242
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
245 |
|
251 | 246 |
func zshCompGenFuncName(c *Command) string { |
247 |
if c.HasParent() { |
|
248 |
return zshCompGenFuncName(c.Parent()) + "_" + c.Name() |
|
242
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
249 |
} |
251 | 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) |
|
242
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
275 |
} |
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
276 |
|
251 | 277 |
func zshCompGenFlagEntryForSingleOptionFlag(f *pflag.Flag) string { |
278 |
var option, multiMark, extras string |
|
242
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
279 |
|
251 | 280 |
if zshCompFlagCouldBeSpecifiedMoreThenOnce(f) { |
281 |
multiMark = "*" |
|
282 |
} |
|
242
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
283 |
|
251 | 284 |
option = "--" + f.Name |
285 |
if option == "--" { |
|
286 |
option = "-" + f.Shorthand |
|
242
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
287 |
} |
251 | 288 |
extras = zshCompGenFlagEntryExtras(f) |
242
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
289 |
|
251 | 290 |
return fmt.Sprintf(`'%s%s[%s]%s'`, multiMark, option, zshCompQuoteFlagDescription(f.Usage), extras) |
242
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
291 |
} |
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
292 |
|
251 | 293 |
func zshCompGenFlagEntryForMultiOptionFlag(f *pflag.Flag) string { |
294 |
var options, parenMultiMark, curlyMultiMark, extras string |
|
295 |
||
296 |
if zshCompFlagCouldBeSpecifiedMoreThenOnce(f) { |
|
297 |
parenMultiMark = "*" |
|
298 |
curlyMultiMark = "\\*" |
|
242
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
299 |
} |
251 | 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) |
|
242
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
306 |
} |
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
307 |
|
251 | 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 |
} |
|
242
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
323 |
} |
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
324 |
} |
251 | 325 |
|
326 |
return extras |
|
242
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
327 |
} |
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
328 |
|
251 | 329 |
func zshCompFlagCouldBeSpecifiedMoreThenOnce(f *pflag.Flag) bool { |
330 |
return strings.Contains(f.Value.Type(), "Slice") || |
|
331 |
strings.Contains(f.Value.Type(), "Array") |
|
242
2a9ec03fe5a1
Use vendoring for backward compatibility
Mikael Berthe <mikael@lilotux.net>
parents:
diff
changeset
|
332 |
} |
251 | 333 |
|
334 |
func zshCompQuoteFlagDescription(s string) string { |
|
335 |
return strings.Replace(s, "'", `'\''`, -1) |
|
336 |
} |