1use std::io::Write;
2
3use clap::*;
4
5use crate::generator::{utils, Generator};
6use crate::INTERNAL_ERROR_MSG;
7
8/// Generate zsh completion file
9#[derive(Copy, Clone, PartialEq, Eq, Debug)]
10pub struct Zsh;
11
12impl Generator for Zsh {
13 fn file_name(&self, name: &str) -> String {
14 format!("_{name}")
15 }
16
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
27autoload -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
46if [ \"$funcstack[1]\" = \"_{name}\" ]; then
47 _{name} \"$@\"
48else
49 compdef _{name} {name}
50fi
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//
89fn 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'
149fn 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
217fn 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 ;;
271esac",
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.
283fn 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: &Command in parent.get_subcommands() {
291 if let Some(ret: &Command) = parser_of(parent: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
319fn 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
358fn value_completion(arg: &Arg) -> Option<String> {
359 if let Some(values) = crate::generator::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
423fn escape_help(string: &str) -> String {
424 string
425 .replace('\\', "\\\\")
426 .replace('\'', "'\\''")
427 .replace('[', "\\[")
428 .replace(']', "\\]")
429 .replace(':', "\\:")
430 .replace('$', "\\$")
431 .replace(from:'`', to:"\\`")
432}
433
434/// Escape value string inside single quotes and parentheses
435fn escape_value(string: &str) -> String {
436 string
437 .replace('\\', "\\\\")
438 .replace('\'', "'\\''")
439 .replace('[', "\\[")
440 .replace(']', "\\]")
441 .replace(':', "\\:")
442 .replace('$', "\\$")
443 .replace('`', "\\`")
444 .replace('(', "\\(")
445 .replace(')', "\\)")
446 .replace(from:' ', to:"\\ ")
447}
448
449fn write_opts_of(p: &Command, p_global: Option<&Command>) -> String {
450 debug!("write_opts_of");
451
452 let mut ret = vec![];
453
454 for o in p.get_opts() {
455 debug!("write_opts_of:iter: o={}", o.get_id());
456
457 let help = escape_help(&o.get_help().unwrap_or_default().to_string());
458 let conflicts = arg_conflicts(p, o, p_global);
459
460 let multiple = if let ArgAction::Count | ArgAction::Append = o.get_action() {
461 "*"
462 } else {
463 ""
464 };
465
466 let vn = match o.get_value_names() {
467 None => " ".to_string(),
468 Some(val) => val[0].to_string(),
469 };
470 let vc = match value_completion(o) {
471 Some(val) => format!(":{vn}:{val}"),
472 None => format!(":{vn}: "),
473 };
474 let vc = vc.repeat(o.get_num_args().expect("built").min_values());
475
476 if let Some(shorts) = o.get_short_and_visible_aliases() {
477 for short in shorts {
478 let s = format!("'{conflicts}{multiple}-{short}+[{help}]{vc}' \\");
479
480 debug!("write_opts_of:iter: Wrote...{}", &*s);
481 ret.push(s);
482 }
483 }
484 if let Some(longs) = o.get_long_and_visible_aliases() {
485 for long in longs {
486 let l = format!("'{conflicts}{multiple}--{long}=[{help}]{vc}' \\");
487
488 debug!("write_opts_of:iter: Wrote...{}", &*l);
489 ret.push(l);
490 }
491 }
492 }
493
494 ret.join("\n")
495}
496
497fn arg_conflicts(cmd: &Command, arg: &Arg, app_global: Option<&Command>) -> String {
498 fn push_conflicts(conflicts: &[&Arg], res: &mut Vec<String>) {
499 for conflict in conflicts {
500 if let Some(s) = conflict.get_short() {
501 res.push(format!("-{s}"));
502 }
503
504 if let Some(l) = conflict.get_long() {
505 res.push(format!("--{l}"));
506 }
507 }
508 }
509
510 let mut res = vec![];
511 match (app_global, arg.is_global_set()) {
512 (Some(x), true) => {
513 let conflicts = x.get_arg_conflicts_with(arg);
514
515 if conflicts.is_empty() {
516 return String::new();
517 }
518
519 push_conflicts(&conflicts, &mut res);
520 }
521 (_, _) => {
522 let conflicts = cmd.get_arg_conflicts_with(arg);
523
524 if conflicts.is_empty() {
525 return String::new();
526 }
527
528 push_conflicts(&conflicts, &mut res);
529 }
530 };
531
532 format!("({})", res.join(" "))
533}
534
535fn write_flags_of(p: &Command, p_global: Option<&Command>) -> String {
536 debug!("write_flags_of;");
537
538 let mut ret = vec![];
539
540 for f in utils::flags(p) {
541 debug!("write_flags_of:iter: f={}", f.get_id());
542
543 let help = escape_help(&f.get_help().unwrap_or_default().to_string());
544 let conflicts = arg_conflicts(p, &f, p_global);
545
546 let multiple = if let ArgAction::Count | ArgAction::Append = f.get_action() {
547 "*"
548 } else {
549 ""
550 };
551
552 if let Some(short) = f.get_short() {
553 let s = format!("'{conflicts}{multiple}-{short}[{help}]' \\");
554
555 debug!("write_flags_of:iter: Wrote...{}", &*s);
556
557 ret.push(s);
558
559 if let Some(short_aliases) = f.get_visible_short_aliases() {
560 for alias in short_aliases {
561 let s = format!("'{conflicts}{multiple}-{alias}[{help}]' \\",);
562
563 debug!("write_flags_of:iter: Wrote...{}", &*s);
564
565 ret.push(s);
566 }
567 }
568 }
569
570 if let Some(long) = f.get_long() {
571 let l = format!("'{conflicts}{multiple}--{long}[{help}]' \\");
572
573 debug!("write_flags_of:iter: Wrote...{}", &*l);
574
575 ret.push(l);
576
577 if let Some(aliases) = f.get_visible_aliases() {
578 for alias in aliases {
579 let l = format!("'{conflicts}{multiple}--{alias}[{help}]' \\");
580
581 debug!("write_flags_of:iter: Wrote...{}", &*l);
582
583 ret.push(l);
584 }
585 }
586 }
587 }
588
589 ret.join("\n")
590}
591
592fn write_positionals_of(p: &Command) -> String {
593 debug!("write_positionals_of;");
594
595 let mut ret = vec![];
596
597 // Completions for commands that end with two Vec arguments require special care.
598 // - You can have two Vec args separated with a custom value terminator.
599 // - You can have two Vec args with the second one set to last (raw sets last)
600 // which will require a '--' separator to be used before the second argument
601 // on the command-line.
602 //
603 // We use the '-S' _arguments option to disable completion after '--'. Thus, the
604 // completion for the second argument in scenario (B) does not need to be emitted
605 // because it is implicitly handled by the '-S' option.
606 // We only need to emit the first catch-all.
607 //
608 // Have we already emitted a catch-all multi-valued positional argument
609 // without a custom value terminator?
610 let mut catch_all_emitted = false;
611
612 for arg in p.get_positionals() {
613 debug!("write_positionals_of:iter: arg={}", arg.get_id());
614
615 let num_args = arg.get_num_args().expect("built");
616 let is_multi_valued = num_args.max_values() > 1;
617
618 if catch_all_emitted && (arg.is_last_set() || is_multi_valued) {
619 // This is the final argument and it also takes multiple arguments.
620 // We've already emitted a catch-all positional argument so we don't need
621 // to emit anything for this argument because it is implicitly handled by
622 // the use of the '-S' _arguments option.
623 continue;
624 }
625
626 let cardinality_value;
627 // If we have any subcommands, we'll emit a catch-all argument, so we shouldn't
628 // emit one here.
629 let cardinality = if is_multi_valued && !p.has_subcommands() {
630 match arg.get_value_terminator() {
631 Some(terminator) => {
632 cardinality_value = format!("*{}:", escape_value(terminator));
633 cardinality_value.as_str()
634 }
635 None => {
636 catch_all_emitted = true;
637 "*:"
638 }
639 }
640 } else if !arg.is_required_set() {
641 ":"
642 } else {
643 ""
644 };
645
646 let a = format!(
647 "'{cardinality}:{name}{help}:{value_completion}' \\",
648 cardinality = cardinality,
649 name = arg.get_id(),
650 help = arg
651 .get_help()
652 .map(|s| s.to_string())
653 .map(|v| " -- ".to_owned() + &v)
654 .unwrap_or_else(|| "".to_owned())
655 .replace('[', "\\[")
656 .replace(']', "\\]")
657 .replace('\'', "'\\''")
658 .replace(':', "\\:"),
659 value_completion = value_completion(arg).unwrap_or_default()
660 );
661
662 debug!("write_positionals_of:iter: Wrote...{a}");
663
664 ret.push(a);
665 }
666
667 ret.join("\n")
668}
669
670#[cfg(test)]
671mod tests {
672 use crate::shells::zsh::{escape_help, escape_value};
673
674 #[test]
675 fn test_escape_value() {
676 let raw_string = "\\ [foo]() `bar https://$PATH";
677 assert_eq!(
678 escape_value(raw_string),
679 "\\\\\\ \\[foo\\]\\(\\)\\ \\`bar\\ https\\://\\$PATH"
680 )
681 }
682
683 #[test]
684 fn test_escape_help() {
685 let raw_string = "\\ [foo]() `bar https://$PATH";
686 assert_eq!(
687 escape_help(raw_string),
688 "\\\\ \\[foo\\]() \\`bar https\\://\\$PATH"
689 )
690 }
691}
692