clap_complete/aot/shells/
zsh.rs
1use std::io::{Error, Write};
2
3use clap::{Arg, ArgAction, Command, ValueHint};
4
5use crate::generator::{utils, Generator};
6use crate::INTERNAL_ERROR_MSG;
7
8#[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 self.try_generate(cmd, buf)
19 .expect("failed to write completion file");
20 }
21
22 fn try_generate(&self, cmd: &Command, buf: &mut dyn Write) -> Result<(), Error> {
23 let bin_name = cmd
24 .get_bin_name()
25 .expect("crate::generate should have set the bin_name");
26
27 write!(
28 buf,
29 "#compdef {name}
30
31autoload -U is-at-least
32
33_{name}() {{
34 typeset -A opt_args
35 typeset -a _arguments_options
36 local ret=1
37
38 if is-at-least 5.2; then
39 _arguments_options=(-s -S -C)
40 else
41 _arguments_options=(-s -C)
42 fi
43
44 local context curcontext=\"$curcontext\" state line
45 {initial_args}{subcommands}
46}}
47
48{subcommand_details}
49
50if [ \"$funcstack[1]\" = \"_{name}\" ]; then
51 _{name} \"$@\"
52else
53 compdef _{name} {name}
54fi
55",
56 name = bin_name,
57 initial_args = get_args_of(cmd, None),
58 subcommands = get_subcommands_of(cmd),
59 subcommand_details = subcommand_details(cmd)
60 )
61 }
62}
63
64fn subcommand_details(p: &Command) -> String {
92 debug!("subcommand_details");
93
94 let bin_name = p
95 .get_bin_name()
96 .expect("crate::generate should have set the bin_name");
97
98 let mut ret = vec![];
99
100 let parent_text = format!(
102 "\
103(( $+functions[_{bin_name_underscore}_commands] )) ||
104_{bin_name_underscore}_commands() {{
105 local commands; commands=({subcommands_and_args})
106 _describe -t commands '{bin_name} commands' commands \"$@\"
107}}",
108 bin_name_underscore = bin_name.replace(' ', "__"),
109 bin_name = bin_name,
110 subcommands_and_args = subcommands_of(p)
111 );
112 ret.push(parent_text);
113
114 let mut all_subcommand_bins: Vec<_> = utils::all_subcommands(p)
116 .into_iter()
117 .map(|(_sc_name, bin_name)| bin_name)
118 .collect();
119
120 all_subcommand_bins.sort();
121 all_subcommand_bins.dedup();
122
123 for bin_name in &all_subcommand_bins {
124 debug!("subcommand_details:iter: bin_name={bin_name}");
125
126 ret.push(format!(
127 "\
128(( $+functions[_{bin_name_underscore}_commands] )) ||
129_{bin_name_underscore}_commands() {{
130 local commands; commands=({subcommands_and_args})
131 _describe -t commands '{bin_name} commands' commands \"$@\"
132}}",
133 bin_name_underscore = bin_name.replace(' ', "__"),
134 bin_name = bin_name,
135 subcommands_and_args =
136 subcommands_of(parser_of(p, bin_name).expect(INTERNAL_ERROR_MSG))
137 ));
138 }
139
140 ret.join("\n")
141}
142
143fn subcommands_of(p: &Command) -> String {
155 debug!("subcommands_of");
156
157 let mut segments = vec![];
158
159 fn add_subcommands(subcommand: &Command, name: &str, ret: &mut Vec<String>) {
160 debug!("add_subcommands");
161
162 let text = format!(
163 "'{name}:{help}' \\",
164 name = name,
165 help = escape_help(&subcommand.get_about().unwrap_or_default().to_string())
166 );
167
168 ret.push(text);
169 }
170
171 for command in p.get_subcommands() {
173 debug!("subcommands_of:iter: subcommand={}", command.get_name());
174
175 add_subcommands(command, command.get_name(), &mut segments);
176
177 for alias in command.get_visible_aliases() {
178 add_subcommands(command, alias, &mut segments);
179 }
180 }
181
182 if !segments.is_empty() {
186 segments.insert(0, "".to_string());
187 segments.push(" ".to_string());
188 }
189
190 segments.join("\n")
191}
192
193fn get_subcommands_of(parent: &Command) -> String {
223 debug!(
224 "get_subcommands_of: Has subcommands...{:?}",
225 parent.has_subcommands()
226 );
227
228 if !parent.has_subcommands() {
229 return String::new();
230 }
231
232 let subcommand_names = utils::subcommands(parent);
233 let mut all_subcommands = vec![];
234
235 for (ref name, ref bin_name) in &subcommand_names {
236 debug!(
237 "get_subcommands_of:iter: parent={}, name={name}, bin_name={bin_name}",
238 parent.get_name(),
239 );
240 let mut segments = vec![format!("({name})")];
241 let subcommand_args = get_args_of(
242 parser_of(parent, bin_name).expect(INTERNAL_ERROR_MSG),
243 Some(parent),
244 );
245
246 if !subcommand_args.is_empty() {
247 segments.push(subcommand_args);
248 }
249
250 let children = get_subcommands_of(parser_of(parent, bin_name).expect(INTERNAL_ERROR_MSG));
252
253 if !children.is_empty() {
254 segments.push(children);
255 }
256
257 segments.push(String::from(";;"));
258 all_subcommands.push(segments.join("\n"));
259 }
260
261 let parent_bin_name = parent
262 .get_bin_name()
263 .expect("crate::generate should have set the bin_name");
264
265 format!(
266 "
267 case $state in
268 ({name})
269 words=($line[{pos}] \"${{words[@]}}\")
270 (( CURRENT += 1 ))
271 curcontext=\"${{curcontext%:*:*}}:{name_hyphen}-command-$line[{pos}]:\"
272 case $line[{pos}] in
273 {subcommands}
274 esac
275 ;;
276esac",
277 name = parent.get_name(),
278 name_hyphen = parent_bin_name.replace(' ', "-"),
279 subcommands = all_subcommands.join("\n"),
280 pos = parent.get_positionals().count() + 1
281 )
282}
283
284fn parser_of<'cmd>(parent: &'cmd Command, bin_name: &str) -> Option<&'cmd Command> {
289 debug!("parser_of: p={}, bin_name={}", parent.get_name(), bin_name);
290
291 if bin_name == parent.get_bin_name().unwrap_or_default() {
292 return Some(parent);
293 }
294
295 for subcommand in parent.get_subcommands() {
296 if let Some(ret) = parser_of(subcommand, bin_name) {
297 return Some(ret);
298 }
299 }
300
301 None
302}
303
304fn get_args_of(parent: &Command, p_global: Option<&Command>) -> String {
325 debug!("get_args_of");
326
327 let mut segments = vec![String::from("_arguments \"${_arguments_options[@]}\" : \\")];
328 let opts = write_opts_of(parent, p_global);
329 let flags = write_flags_of(parent, p_global);
330 let positionals = write_positionals_of(parent);
331
332 if !opts.is_empty() {
333 segments.push(opts);
334 }
335
336 if !flags.is_empty() {
337 segments.push(flags);
338 }
339
340 if !positionals.is_empty() {
341 segments.push(positionals);
342 }
343
344 if parent.has_subcommands() {
345 let parent_bin_name = parent
346 .get_bin_name()
347 .expect("crate::generate should have set the bin_name");
348 let subcommand_bin_name = format!(
349 "\":: :_{name}_commands\" \\",
350 name = parent_bin_name.replace(' ', "__")
351 );
352 segments.push(subcommand_bin_name);
353
354 let subcommand_text = format!("\"*::: :->{name}\" \\", name = parent.get_name());
355 segments.push(subcommand_text);
356 } else if parent.is_allow_external_subcommands_set() {
357 segments.push(String::from("\"*::external_command:_default\" \\"));
360 }
361
362 segments.push(String::from("&& ret=0"));
363 segments.join("\n")
364}
365
366fn value_completion(arg: &Arg) -> Option<String> {
368 if let Some(values) = utils::possible_values(arg) {
369 if values
370 .iter()
371 .any(|value| !value.is_hide_set() && value.get_help().is_some())
372 {
373 Some(format!(
374 "(({}))",
375 values
376 .iter()
377 .filter_map(|value| {
378 if value.is_hide_set() {
379 None
380 } else {
381 Some(format!(
382 r#"{name}\:"{tooltip}""#,
383 name = escape_value(value.get_name()),
384 tooltip =
385 escape_help(&value.get_help().unwrap_or_default().to_string()),
386 ))
387 }
388 })
389 .collect::<Vec<_>>()
390 .join("\n")
391 ))
392 } else {
393 Some(format!(
394 "({})",
395 values
396 .iter()
397 .filter(|pv| !pv.is_hide_set())
398 .map(|n| n.get_name())
399 .collect::<Vec<_>>()
400 .join(" ")
401 ))
402 }
403 } else {
404 Some(
406 match arg.get_value_hint() {
407 ValueHint::Unknown => "_default",
408 ValueHint::Other => "",
409 ValueHint::AnyPath => "_files",
410 ValueHint::FilePath => "_files",
411 ValueHint::DirPath => "_files -/",
412 ValueHint::ExecutablePath => "_absolute_command_paths",
413 ValueHint::CommandName => "_command_names -e",
414 ValueHint::CommandString => "_cmdstring",
415 ValueHint::CommandWithArguments => "_cmdambivalent",
416 ValueHint::Username => "_users",
417 ValueHint::Hostname => "_hosts",
418 ValueHint::Url => "_urls",
419 ValueHint::EmailAddress => "_email_addresses",
420 _ => {
421 return None;
422 }
423 }
424 .to_string(),
425 )
426 }
427}
428
429fn escape_help(string: &str) -> String {
431 string
432 .replace('\\', "\\\\")
433 .replace('\'', "'\\''")
434 .replace('[', "\\[")
435 .replace(']', "\\]")
436 .replace(':', "\\:")
437 .replace('$', "\\$")
438 .replace('`', "\\`")
439 .replace('\n', " ")
440}
441
442fn escape_value(string: &str) -> String {
444 string
445 .replace('\\', "\\\\")
446 .replace('\'', "'\\''")
447 .replace('[', "\\[")
448 .replace(']', "\\]")
449 .replace(':', "\\:")
450 .replace('$', "\\$")
451 .replace('`', "\\`")
452 .replace('(', "\\(")
453 .replace(')', "\\)")
454 .replace(' ', "\\ ")
455}
456
457fn write_opts_of(p: &Command, p_global: Option<&Command>) -> String {
458 debug!("write_opts_of");
459
460 let mut ret = vec![];
461
462 for o in p.get_opts() {
463 debug!("write_opts_of:iter: o={}", o.get_id());
464
465 let help = escape_help(&o.get_help().unwrap_or_default().to_string());
466 let conflicts = arg_conflicts(p, o, p_global);
467
468 let multiple = if let ArgAction::Count | ArgAction::Append = o.get_action() {
469 "*"
470 } else {
471 ""
472 };
473
474 let vn = match o.get_value_names() {
475 None => " ".to_string(),
476 Some(val) => val[0].to_string(),
477 };
478 let vc = match value_completion(o) {
479 Some(val) => format!(":{vn}:{val}"),
480 None => format!(":{vn}: "),
481 };
482 let vc = vc.repeat(o.get_num_args().expect("built").min_values());
483
484 if let Some(shorts) = o.get_short_and_visible_aliases() {
485 for short in shorts {
486 let s = format!("'{conflicts}{multiple}-{short}+[{help}]{vc}' \\");
487
488 debug!("write_opts_of:iter: Wrote...{}", &*s);
489 ret.push(s);
490 }
491 }
492 if let Some(longs) = o.get_long_and_visible_aliases() {
493 for long in longs {
494 let l = format!("'{conflicts}{multiple}--{long}=[{help}]{vc}' \\");
495
496 debug!("write_opts_of:iter: Wrote...{}", &*l);
497 ret.push(l);
498 }
499 }
500 }
501
502 ret.join("\n")
503}
504
505fn arg_conflicts(cmd: &Command, arg: &Arg, app_global: Option<&Command>) -> String {
506 fn push_conflicts(conflicts: &[&Arg], res: &mut Vec<String>) {
507 for conflict in conflicts {
508 if let Some(s) = conflict.get_short() {
509 res.push(format!("-{s}"));
510 }
511
512 if let Some(l) = conflict.get_long() {
513 res.push(format!("--{l}"));
514 }
515 }
516 }
517
518 let mut res = vec![];
519 match (app_global, arg.is_global_set()) {
520 (Some(x), true) => {
521 let conflicts = x.get_arg_conflicts_with(arg);
522
523 if conflicts.is_empty() {
524 return String::new();
525 }
526
527 push_conflicts(&conflicts, &mut res);
528 }
529 (_, _) => {
530 let conflicts = cmd.get_arg_conflicts_with(arg);
531
532 if conflicts.is_empty() {
533 return String::new();
534 }
535
536 push_conflicts(&conflicts, &mut res);
537 }
538 };
539
540 format!("({})", res.join(" "))
541}
542
543fn write_flags_of(p: &Command, p_global: Option<&Command>) -> String {
544 debug!("write_flags_of;");
545
546 let mut ret = vec![];
547
548 for f in utils::flags(p) {
549 debug!("write_flags_of:iter: f={}", f.get_id());
550
551 let help = escape_help(&f.get_help().unwrap_or_default().to_string());
552 let conflicts = arg_conflicts(p, &f, p_global);
553
554 let multiple = if let ArgAction::Count | ArgAction::Append = f.get_action() {
555 "*"
556 } else {
557 ""
558 };
559
560 if let Some(short) = f.get_short() {
561 let s = format!("'{conflicts}{multiple}-{short}[{help}]' \\");
562
563 debug!("write_flags_of:iter: Wrote...{}", &*s);
564
565 ret.push(s);
566
567 if let Some(short_aliases) = f.get_visible_short_aliases() {
568 for alias in short_aliases {
569 let s = format!("'{conflicts}{multiple}-{alias}[{help}]' \\",);
570
571 debug!("write_flags_of:iter: Wrote...{}", &*s);
572
573 ret.push(s);
574 }
575 }
576 }
577
578 if let Some(long) = f.get_long() {
579 let l = format!("'{conflicts}{multiple}--{long}[{help}]' \\");
580
581 debug!("write_flags_of:iter: Wrote...{}", &*l);
582
583 ret.push(l);
584
585 if let Some(aliases) = f.get_visible_aliases() {
586 for alias in aliases {
587 let l = format!("'{conflicts}{multiple}--{alias}[{help}]' \\");
588
589 debug!("write_flags_of:iter: Wrote...{}", &*l);
590
591 ret.push(l);
592 }
593 }
594 }
595 }
596
597 ret.join("\n")
598}
599
600fn write_positionals_of(p: &Command) -> String {
601 debug!("write_positionals_of;");
602
603 let mut ret = vec![];
604
605 let mut catch_all_emitted = false;
619
620 for arg in p.get_positionals() {
621 debug!("write_positionals_of:iter: arg={}", arg.get_id());
622
623 let num_args = arg.get_num_args().expect("built");
624 let is_multi_valued = num_args.max_values() > 1;
625
626 if catch_all_emitted && (arg.is_last_set() || is_multi_valued) {
627 continue;
632 }
633
634 let cardinality_value;
635 let cardinality = if is_multi_valued && !p.has_subcommands() {
638 match arg.get_value_terminator() {
639 Some(terminator) => {
640 cardinality_value = format!("*{}:", escape_value(terminator));
641 cardinality_value.as_str()
642 }
643 None => {
644 catch_all_emitted = true;
645 "*:"
646 }
647 }
648 } else if !arg.is_required_set() {
649 ":"
650 } else {
651 ""
652 };
653
654 let a = format!(
655 "'{cardinality}:{name}{help}:{value_completion}' \\",
656 cardinality = cardinality,
657 name = arg.get_id(),
658 help = arg
659 .get_help()
660 .map(|s| s.to_string())
661 .map(|v| " -- ".to_owned() + &v)
662 .unwrap_or_else(|| "".to_owned())
663 .replace('[', "\\[")
664 .replace(']', "\\]")
665 .replace('\'', "'\\''")
666 .replace(':', "\\:"),
667 value_completion = value_completion(arg).unwrap_or_default()
668 );
669
670 debug!("write_positionals_of:iter: Wrote...{a}");
671
672 ret.push(a);
673 }
674
675 ret.join("\n")
676}
677
678#[cfg(test)]
679mod tests {
680 use super::{escape_help, escape_value};
681
682 #[test]
683 fn test_escape_value() {
684 let raw_string = "\\ [foo]() `bar https://$PATH";
685 assert_eq!(
686 escape_value(raw_string),
687 "\\\\\\ \\[foo\\]\\(\\)\\ \\`bar\\ https\\://\\$PATH"
688 );
689 }
690
691 #[test]
692 fn test_escape_help() {
693 let raw_string = "\\ [foo]() `bar https://$PATH";
694 assert_eq!(
695 escape_help(raw_string),
696 "\\\\ \\[foo\\]() \\`bar https\\://\\$PATH"
697 );
698 }
699}