1 use std::io::Write;
2
3 use clap::{Arg, ArgAction, Command, ValueHint};
4
5 use crate::generator::{utils, Generator};
6 use crate::INTERNAL_ERROR_MSG;
7
8 /// Generate zsh completion file
9 #[derive(Copy, Clone, PartialEq, Eq, Debug)]
10 pub struct Zsh;
11
12 impl Generator for Zsh {
file_name(&self, name: &str) -> String13 fn file_name(&self, name: &str) -> String {
14 format!("_{name}")
15 }
16
generate(&self, cmd: &Command, buf: &mut dyn Write)17 fn generate(&self, cmd: &Command, buf: &mut dyn Write) {
18 let bin_name = cmd
19 .get_bin_name()
20 .expect("crate::generate should have set the bin_name");
21
22 w!(
23 buf,
24 format!(
25 "#compdef {name}
26
27 autoload -U is-at-least
28
29 _{name}() {{
30 typeset -A opt_args
31 typeset -a _arguments_options
32 local ret=1
33
34 if is-at-least 5.2; then
35 _arguments_options=(-s -S -C)
36 else
37 _arguments_options=(-s -C)
38 fi
39
40 local context curcontext=\"$curcontext\" state line
41 {initial_args}{subcommands}
42 }}
43
44 {subcommand_details}
45
46 if [ \"$funcstack[1]\" = \"_{name}\" ]; then
47 _{name} \"$@\"
48 else
49 compdef _{name} {name}
50 fi
51 ",
52 name = bin_name,
53 initial_args = get_args_of(cmd, None),
54 subcommands = get_subcommands_of(cmd),
55 subcommand_details = subcommand_details(cmd)
56 )
57 .as_bytes()
58 );
59 }
60 }
61
62 // Displays the commands of a subcommand
63 // (( $+functions[_[bin_name_underscore]_commands] )) ||
64 // _[bin_name_underscore]_commands() {
65 // local commands; commands=(
66 // '[arg_name]:[arg_help]'
67 // )
68 // _describe -t commands '[bin_name] commands' commands "$@"
69 //
70 // Where the following variables are present:
71 // [bin_name_underscore]: The full space delineated bin_name, where spaces have been replaced by
72 // underscore characters
73 // [arg_name]: The name of the subcommand
74 // [arg_help]: The help message of the subcommand
75 // [bin_name]: The full space delineated bin_name
76 //
77 // Here's a snippet from rustup:
78 //
79 // (( $+functions[_rustup_commands] )) ||
80 // _rustup_commands() {
81 // local commands; commands=(
82 // 'show:Show the active and installed toolchains'
83 // 'update:Update Rust toolchains'
84 // # ... snip for brevity
85 // 'help:Print this message or the help of the given subcommand(s)'
86 // )
87 // _describe -t commands 'rustup commands' commands "$@"
88 //
subcommand_details(p: &Command) -> String89 fn subcommand_details(p: &Command) -> String {
90 debug!("subcommand_details");
91
92 let bin_name = p
93 .get_bin_name()
94 .expect("crate::generate should have set the bin_name");
95
96 let mut ret = vec![];
97
98 // First we do ourself
99 let parent_text = format!(
100 "\
101 (( $+functions[_{bin_name_underscore}_commands] )) ||
102 _{bin_name_underscore}_commands() {{
103 local commands; commands=({subcommands_and_args})
104 _describe -t commands '{bin_name} commands' commands \"$@\"
105 }}",
106 bin_name_underscore = bin_name.replace(' ', "__"),
107 bin_name = bin_name,
108 subcommands_and_args = subcommands_of(p)
109 );
110 ret.push(parent_text);
111
112 // Next we start looping through all the children, grandchildren, etc.
113 let mut all_subcommands = utils::all_subcommands(p);
114
115 all_subcommands.sort();
116 all_subcommands.dedup();
117
118 for (_, ref bin_name) in &all_subcommands {
119 debug!("subcommand_details:iter: bin_name={bin_name}");
120
121 ret.push(format!(
122 "\
123 (( $+functions[_{bin_name_underscore}_commands] )) ||
124 _{bin_name_underscore}_commands() {{
125 local commands; commands=({subcommands_and_args})
126 _describe -t commands '{bin_name} commands' commands \"$@\"
127 }}",
128 bin_name_underscore = bin_name.replace(' ', "__"),
129 bin_name = bin_name,
130 subcommands_and_args =
131 subcommands_of(parser_of(p, bin_name).expect(INTERNAL_ERROR_MSG))
132 ));
133 }
134
135 ret.join("\n")
136 }
137
138 // Generates subcommand completions in form of
139 //
140 // '[arg_name]:[arg_help]'
141 //
142 // Where:
143 // [arg_name]: the subcommand's name
144 // [arg_help]: the help message of the subcommand
145 //
146 // A snippet from rustup:
147 // 'show:Show the active and installed toolchains'
148 // 'update:Update Rust toolchains'
subcommands_of(p: &Command) -> String149 fn subcommands_of(p: &Command) -> String {
150 debug!("subcommands_of");
151
152 let mut segments = vec![];
153
154 fn add_subcommands(subcommand: &Command, name: &str, ret: &mut Vec<String>) {
155 debug!("add_subcommands");
156
157 let text = format!(
158 "'{name}:{help}' \\",
159 name = name,
160 help = escape_help(&subcommand.get_about().unwrap_or_default().to_string())
161 );
162
163 ret.push(text);
164 }
165
166 // The subcommands
167 for command in p.get_subcommands() {
168 debug!("subcommands_of:iter: subcommand={}", command.get_name());
169
170 add_subcommands(command, command.get_name(), &mut segments);
171
172 for alias in command.get_visible_aliases() {
173 add_subcommands(command, alias, &mut segments);
174 }
175 }
176
177 // Surround the text with newlines for proper formatting.
178 // We need this to prevent weirdly formatted `command=(\n \n)` sections.
179 // When there are no (sub-)commands.
180 if !segments.is_empty() {
181 segments.insert(0, "".to_string());
182 segments.push(" ".to_string());
183 }
184
185 segments.join("\n")
186 }
187
188 // Get's the subcommand section of a completion file
189 // This looks roughly like:
190 //
191 // case $state in
192 // ([bin_name]_args)
193 // curcontext=\"${curcontext%:*:*}:[name_hyphen]-command-$words[1]:\"
194 // case $line[1] in
195 //
196 // ([name])
197 // _arguments -C -s -S \
198 // [subcommand_args]
199 // && ret=0
200 //
201 // [RECURSIVE_CALLS]
202 //
203 // ;;",
204 //
205 // [repeat]
206 //
207 // esac
208 // ;;
209 // esac",
210 //
211 // Where the following variables are present:
212 // [name] = The subcommand name in the form of "install" for "rustup toolchain install"
213 // [bin_name] = The full space delineated bin_name such as "rustup toolchain install"
214 // [name_hyphen] = The full space delineated bin_name, but replace spaces with hyphens
215 // [repeat] = From the same recursive calls, but for all subcommands
216 // [subcommand_args] = The same as zsh::get_args_of
217 fn get_subcommands_of(parent: &Command) -> String {
218 debug!(
219 "get_subcommands_of: Has subcommands...{:?}",
220 parent.has_subcommands()
221 );
222
223 if !parent.has_subcommands() {
224 return String::new();
225 }
226
227 let subcommand_names = utils::subcommands(parent);
228 let mut all_subcommands = vec![];
229
230 for (ref name, ref bin_name) in &subcommand_names {
231 debug!(
232 "get_subcommands_of:iter: parent={}, name={name}, bin_name={bin_name}",
233 parent.get_name(),
234 );
235 let mut segments = vec![format!("({name})")];
236 let subcommand_args = get_args_of(
237 parser_of(parent, bin_name).expect(INTERNAL_ERROR_MSG),
238 Some(parent),
239 );
240
241 if !subcommand_args.is_empty() {
242 segments.push(subcommand_args);
243 }
244
245 // Get the help text of all child subcommands.
246 let children = get_subcommands_of(parser_of(parent, bin_name).expect(INTERNAL_ERROR_MSG));
247
248 if !children.is_empty() {
249 segments.push(children);
250 }
251
252 segments.push(String::from(";;"));
253 all_subcommands.push(segments.join("\n"));
254 }
255
256 let parent_bin_name = parent
257 .get_bin_name()
258 .expect("crate::generate should have set the bin_name");
259
260 format!(
261 "
262 case $state in
263 ({name})
264 words=($line[{pos}] \"${{words[@]}}\")
265 (( CURRENT += 1 ))
266 curcontext=\"${{curcontext%:*:*}}:{name_hyphen}-command-$line[{pos}]:\"
267 case $line[{pos}] in
268 {subcommands}
269 esac
270 ;;
271 esac",
272 name = parent.get_name(),
273 name_hyphen = parent_bin_name.replace(' ', "-"),
274 subcommands = all_subcommands.join("\n"),
275 pos = parent.get_positionals().count() + 1
276 )
277 }
278
279 // Get the Command for a given subcommand tree.
280 //
281 // Given the bin_name "a b c" and the Command for "a" this returns the "c" Command.
282 // Given the bin_name "a b c" and the Command for "b" this returns the "c" Command.
283 fn parser_of<'cmd>(parent: &'cmd Command, bin_name: &str) -> Option<&'cmd Command> {
284 debug!("parser_of: p={}, bin_name={}", parent.get_name(), bin_name);
285
286 if bin_name == parent.get_bin_name().unwrap_or_default() {
287 return Some(parent);
288 }
289
290 for subcommand in parent.get_subcommands() {
291 if let Some(ret) = parser_of(subcommand, bin_name) {
292 return Some(ret);
293 }
294 }
295
296 None
297 }
298
299 // Writes out the args section, which ends up being the flags, opts and positionals, and a jump to
300 // another ZSH function if there are subcommands.
301 // The structure works like this:
302 // ([conflicting_args]) [multiple] arg [takes_value] [[help]] [: :(possible_values)]
303 // ^-- list '-v -h' ^--'*' ^--'+' ^-- list 'one two three'
304 //
305 // An example from the rustup command:
306 //
307 // _arguments -C -s -S \
308 // '(-h --help --verbose)-v[Enable verbose output]' \
309 // '(-V -v --version --verbose --help)-h[Print help information]' \
310 // # ... snip for brevity
311 // ':: :_rustup_commands' \ # <-- displays subcommands
312 // '*::: :->rustup' \ # <-- displays subcommand args and child subcommands
313 // && ret=0
314 //
315 // The args used for _arguments are as follows:
316 // -C: modify the $context internal variable
317 // -s: Allow stacking of short args (i.e. -a -b -c => -abc)
318 // -S: Do not complete anything after '--' and treat those as argument values
319 fn get_args_of(parent: &Command, p_global: Option<&Command>) -> String {
320 debug!("get_args_of");
321
322 let mut segments = vec![String::from("_arguments \"${_arguments_options[@]}\" \\")];
323 let opts = write_opts_of(parent, p_global);
324 let flags = write_flags_of(parent, p_global);
325 let positionals = write_positionals_of(parent);
326
327 if !opts.is_empty() {
328 segments.push(opts);
329 }
330
331 if !flags.is_empty() {
332 segments.push(flags);
333 }
334
335 if !positionals.is_empty() {
336 segments.push(positionals);
337 }
338
339 if parent.has_subcommands() {
340 let parent_bin_name = parent
341 .get_bin_name()
342 .expect("crate::generate should have set the bin_name");
343 let subcommand_bin_name = format!(
344 "\":: :_{name}_commands\" \\",
345 name = parent_bin_name.replace(' ', "__")
346 );
347 segments.push(subcommand_bin_name);
348
349 let subcommand_text = format!("\"*::: :->{name}\" \\", name = parent.get_name());
350 segments.push(subcommand_text);
351 };
352
353 segments.push(String::from("&& ret=0"));
354 segments.join("\n")
355 }
356
357 // Uses either `possible_vals` or `value_hint` to give hints about possible argument values
358 fn value_completion(arg: &Arg) -> Option<String> {
359 if let Some(values) = utils::possible_values(arg) {
360 if values
361 .iter()
362 .any(|value| !value.is_hide_set() && value.get_help().is_some())
363 {
364 Some(format!(
365 "(({}))",
366 values
367 .iter()
368 .filter_map(|value| {
369 if value.is_hide_set() {
370 None
371 } else {
372 Some(format!(
373 r#"{name}\:"{tooltip}""#,
374 name = escape_value(value.get_name()),
375 tooltip =
376 escape_help(&value.get_help().unwrap_or_default().to_string()),
377 ))
378 }
379 })
380 .collect::<Vec<_>>()
381 .join("\n")
382 ))
383 } else {
384 Some(format!(
385 "({})",
386 values
387 .iter()
388 .filter(|pv| !pv.is_hide_set())
389 .map(|n| n.get_name())
390 .collect::<Vec<_>>()
391 .join(" ")
392 ))
393 }
394 } else {
395 // NB! If you change this, please also update the table in `ValueHint` documentation.
396 Some(
397 match arg.get_value_hint() {
398 ValueHint::Unknown => {
399 return None;
400 }
401 ValueHint::Other => "( )",
402 ValueHint::AnyPath => "_files",
403 ValueHint::FilePath => "_files",
404 ValueHint::DirPath => "_files -/",
405 ValueHint::ExecutablePath => "_absolute_command_paths",
406 ValueHint::CommandName => "_command_names -e",
407 ValueHint::CommandString => "_cmdstring",
408 ValueHint::CommandWithArguments => "_cmdambivalent",
409 ValueHint::Username => "_users",
410 ValueHint::Hostname => "_hosts",
411 ValueHint::Url => "_urls",
412 ValueHint::EmailAddress => "_email_addresses",
413 _ => {
414 return None;
415 }
416 }
417 .to_string(),
418 )
419 }
420 }
421
422 /// Escape help string inside single quotes and brackets
423 fn escape_help(string: &str) -> String {
424 string
425 .replace('\\', "\\\\")
426 .replace('\'', "'\\''")
427 .replace('[', "\\[")
428 .replace(']', "\\]")
429 .replace(':', "\\:")
430 .replace('$', "\\$")
431 .replace('`', "\\`")
432 .replace('\n', " ")
433 }
434
435 /// Escape value string inside single quotes and parentheses
436 fn escape_value(string: &str) -> String {
437 string
438 .replace('\\', "\\\\")
439 .replace('\'', "'\\''")
440 .replace('[', "\\[")
441 .replace(']', "\\]")
442 .replace(':', "\\:")
443 .replace('$', "\\$")
444 .replace('`', "\\`")
445 .replace('(', "\\(")
446 .replace(')', "\\)")
447 .replace(' ', "\\ ")
448 }
449
450 fn write_opts_of(p: &Command, p_global: Option<&Command>) -> String {
451 debug!("write_opts_of");
452
453 let mut ret = vec![];
454
455 for o in p.get_opts() {
456 debug!("write_opts_of:iter: o={}", o.get_id());
457
458 let help = escape_help(&o.get_help().unwrap_or_default().to_string());
459 let conflicts = arg_conflicts(p, o, p_global);
460
461 let multiple = if let ArgAction::Count | ArgAction::Append = o.get_action() {
462 "*"
463 } else {
464 ""
465 };
466
467 let vn = match o.get_value_names() {
468 None => " ".to_string(),
469 Some(val) => val[0].to_string(),
470 };
471 let vc = match value_completion(o) {
472 Some(val) => format!(":{vn}:{val}"),
473 None => format!(":{vn}: "),
474 };
475 let vc = vc.repeat(o.get_num_args().expect("built").min_values());
476
477 if let Some(shorts) = o.get_short_and_visible_aliases() {
478 for short in shorts {
479 let s = format!("'{conflicts}{multiple}-{short}+[{help}]{vc}' \\");
480
481 debug!("write_opts_of:iter: Wrote...{}", &*s);
482 ret.push(s);
483 }
484 }
485 if let Some(longs) = o.get_long_and_visible_aliases() {
486 for long in longs {
487 let l = format!("'{conflicts}{multiple}--{long}=[{help}]{vc}' \\");
488
489 debug!("write_opts_of:iter: Wrote...{}", &*l);
490 ret.push(l);
491 }
492 }
493 }
494
495 ret.join("\n")
496 }
497
498 fn arg_conflicts(cmd: &Command, arg: &Arg, app_global: Option<&Command>) -> String {
499 fn push_conflicts(conflicts: &[&Arg], res: &mut Vec<String>) {
500 for conflict in conflicts {
501 if let Some(s) = conflict.get_short() {
502 res.push(format!("-{s}"));
503 }
504
505 if let Some(l) = conflict.get_long() {
506 res.push(format!("--{l}"));
507 }
508 }
509 }
510
511 let mut res = vec![];
512 match (app_global, arg.is_global_set()) {
513 (Some(x), true) => {
514 let conflicts = x.get_arg_conflicts_with(arg);
515
516 if conflicts.is_empty() {
517 return String::new();
518 }
519
520 push_conflicts(&conflicts, &mut res);
521 }
522 (_, _) => {
523 let conflicts = cmd.get_arg_conflicts_with(arg);
524
525 if conflicts.is_empty() {
526 return String::new();
527 }
528
529 push_conflicts(&conflicts, &mut res);
530 }
531 };
532
533 format!("({})", res.join(" "))
534 }
535
536 fn write_flags_of(p: &Command, p_global: Option<&Command>) -> String {
537 debug!("write_flags_of;");
538
539 let mut ret = vec![];
540
541 for f in utils::flags(p) {
542 debug!("write_flags_of:iter: f={}", f.get_id());
543
544 let help = escape_help(&f.get_help().unwrap_or_default().to_string());
545 let conflicts = arg_conflicts(p, &f, p_global);
546
547 let multiple = if let ArgAction::Count | ArgAction::Append = f.get_action() {
548 "*"
549 } else {
550 ""
551 };
552
553 if let Some(short) = f.get_short() {
554 let s = format!("'{conflicts}{multiple}-{short}[{help}]' \\");
555
556 debug!("write_flags_of:iter: Wrote...{}", &*s);
557
558 ret.push(s);
559
560 if let Some(short_aliases) = f.get_visible_short_aliases() {
561 for alias in short_aliases {
562 let s = format!("'{conflicts}{multiple}-{alias}[{help}]' \\",);
563
564 debug!("write_flags_of:iter: Wrote...{}", &*s);
565
566 ret.push(s);
567 }
568 }
569 }
570
571 if let Some(long) = f.get_long() {
572 let l = format!("'{conflicts}{multiple}--{long}[{help}]' \\");
573
574 debug!("write_flags_of:iter: Wrote...{}", &*l);
575
576 ret.push(l);
577
578 if let Some(aliases) = f.get_visible_aliases() {
579 for alias in aliases {
580 let l = format!("'{conflicts}{multiple}--{alias}[{help}]' \\");
581
582 debug!("write_flags_of:iter: Wrote...{}", &*l);
583
584 ret.push(l);
585 }
586 }
587 }
588 }
589
590 ret.join("\n")
591 }
592
593 fn write_positionals_of(p: &Command) -> String {
594 debug!("write_positionals_of;");
595
596 let mut ret = vec![];
597
598 // Completions for commands that end with two Vec arguments require special care.
599 // - You can have two Vec args separated with a custom value terminator.
600 // - You can have two Vec args with the second one set to last (raw sets last)
601 // which will require a '--' separator to be used before the second argument
602 // on the command-line.
603 //
604 // We use the '-S' _arguments option to disable completion after '--'. Thus, the
605 // completion for the second argument in scenario (B) does not need to be emitted
606 // because it is implicitly handled by the '-S' option.
607 // We only need to emit the first catch-all.
608 //
609 // Have we already emitted a catch-all multi-valued positional argument
610 // without a custom value terminator?
611 let mut catch_all_emitted = false;
612
613 for arg in p.get_positionals() {
614 debug!("write_positionals_of:iter: arg={}", arg.get_id());
615
616 let num_args = arg.get_num_args().expect("built");
617 let is_multi_valued = num_args.max_values() > 1;
618
619 if catch_all_emitted && (arg.is_last_set() || is_multi_valued) {
620 // This is the final argument and it also takes multiple arguments.
621 // We've already emitted a catch-all positional argument so we don't need
622 // to emit anything for this argument because it is implicitly handled by
623 // the use of the '-S' _arguments option.
624 continue;
625 }
626
627 let cardinality_value;
628 // If we have any subcommands, we'll emit a catch-all argument, so we shouldn't
629 // emit one here.
630 let cardinality = if is_multi_valued && !p.has_subcommands() {
631 match arg.get_value_terminator() {
632 Some(terminator) => {
633 cardinality_value = format!("*{}:", escape_value(terminator));
634 cardinality_value.as_str()
635 }
636 None => {
637 catch_all_emitted = true;
638 "*:"
639 }
640 }
641 } else if !arg.is_required_set() {
642 ":"
643 } else {
644 ""
645 };
646
647 let a = format!(
648 "'{cardinality}:{name}{help}:{value_completion}' \\",
649 cardinality = cardinality,
650 name = arg.get_id(),
651 help = arg
652 .get_help()
653 .map(|s| s.to_string())
654 .map(|v| " -- ".to_owned() + &v)
655 .unwrap_or_else(|| "".to_owned())
656 .replace('[', "\\[")
657 .replace(']', "\\]")
658 .replace('\'', "'\\''")
659 .replace(':', "\\:"),
660 value_completion = value_completion(arg).unwrap_or_default()
661 );
662
663 debug!("write_positionals_of:iter: Wrote...{a}");
664
665 ret.push(a);
666 }
667
668 ret.join("\n")
669 }
670
671 #[cfg(test)]
672 mod tests {
673 use crate::shells::zsh::{escape_help, escape_value};
674
675 #[test]
676 fn test_escape_value() {
677 let raw_string = "\\ [foo]() `bar https://$PATH";
678 assert_eq!(
679 escape_value(raw_string),
680 "\\\\\\ \\[foo\\]\\(\\)\\ \\`bar\\ https\\://\\$PATH"
681 );
682 }
683
684 #[test]
685 fn test_escape_help() {
686 let raw_string = "\\ [foo]() `bar https://$PATH";
687 assert_eq!(
688 escape_help(raw_string),
689 "\\\\ \\[foo\\]() \\`bar https\\://\\$PATH"
690 );
691 }
692 }
693