clap_complete/aot/shells/
zsh.rs
1use std::io::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 let bin_name = cmd
19 .get_bin_name()
20 .expect("crate::generate should have set the bin_name");
21
22 write!(
23 buf,
24 "#compdef {name}
25
26autoload -U is-at-least
27
28_{name}() {{
29 typeset -A opt_args
30 typeset -a _arguments_options
31 local ret=1
32
33 if is-at-least 5.2; then
34 _arguments_options=(-s -S -C)
35 else
36 _arguments_options=(-s -C)
37 fi
38
39 local context curcontext=\"$curcontext\" state line
40 {initial_args}{subcommands}
41}}
42
43{subcommand_details}
44
45if [ \"$funcstack[1]\" = \"_{name}\" ]; then
46 _{name} \"$@\"
47else
48 compdef _{name} {name}
49fi
50",
51 name = bin_name,
52 initial_args = get_args_of(cmd, None),
53 subcommands = get_subcommands_of(cmd),
54 subcommand_details = subcommand_details(cmd)
55 )
56 .expect("failed to write completion file");
57 }
58}
59
60fn subcommand_details(p: &Command) -> String {
88 debug!("subcommand_details");
89
90 let bin_name = p
91 .get_bin_name()
92 .expect("crate::generate should have set the bin_name");
93
94 let mut ret = vec![];
95
96 let parent_text = format!(
98 "\
99(( $+functions[_{bin_name_underscore}_commands] )) ||
100_{bin_name_underscore}_commands() {{
101 local commands; commands=({subcommands_and_args})
102 _describe -t commands '{bin_name} commands' commands \"$@\"
103}}",
104 bin_name_underscore = bin_name.replace(' ', "__"),
105 bin_name = bin_name,
106 subcommands_and_args = subcommands_of(p)
107 );
108 ret.push(parent_text);
109
110 let mut all_subcommand_bins: Vec<_> = utils::all_subcommands(p)
112 .into_iter()
113 .map(|(_sc_name, bin_name)| bin_name)
114 .collect();
115
116 all_subcommand_bins.sort();
117 all_subcommand_bins.dedup();
118
119 for bin_name in &all_subcommand_bins {
120 debug!("subcommand_details:iter: bin_name={bin_name}");
121
122 ret.push(format!(
123 "\
124(( $+functions[_{bin_name_underscore}_commands] )) ||
125_{bin_name_underscore}_commands() {{
126 local commands; commands=({subcommands_and_args})
127 _describe -t commands '{bin_name} commands' commands \"$@\"
128}}",
129 bin_name_underscore = bin_name.replace(' ', "__"),
130 bin_name = bin_name,
131 subcommands_and_args =
132 subcommands_of(parser_of(p, bin_name).expect(INTERNAL_ERROR_MSG))
133 ));
134 }
135
136 ret.join("\n")
137}
138
139fn subcommands_of(p: &Command) -> String {
151 debug!("subcommands_of");
152
153 let mut segments = vec![];
154
155 fn add_subcommands(subcommand: &Command, name: &str, ret: &mut Vec<String>) {
156 debug!("add_subcommands");
157
158 let text = format!(
159 "'{name}:{help}' \\",
160 name = name,
161 help = escape_help(&subcommand.get_about().unwrap_or_default().to_string())
162 );
163
164 ret.push(text);
165 }
166
167 for command in p.get_subcommands() {
169 debug!("subcommands_of:iter: subcommand={}", command.get_name());
170
171 add_subcommands(command, command.get_name(), &mut segments);
172
173 for alias in command.get_visible_aliases() {
174 add_subcommands(command, alias, &mut segments);
175 }
176 }
177
178 if !segments.is_empty() {
182 segments.insert(0, "".to_string());
183 segments.push(" ".to_string());
184 }
185
186 segments.join("\n")
187}
188
189fn get_subcommands_of(parent: &Command) -> String {
219 debug!(
220 "get_subcommands_of: Has subcommands...{:?}",
221 parent.has_subcommands()
222 );
223
224 if !parent.has_subcommands() {
225 return String::new();
226 }
227
228 let subcommand_names = utils::subcommands(parent);
229 let mut all_subcommands = vec![];
230
231 for (ref name, ref bin_name) in &subcommand_names {
232 debug!(
233 "get_subcommands_of:iter: parent={}, name={name}, bin_name={bin_name}",
234 parent.get_name(),
235 );
236 let mut segments = vec![format!("({name})")];
237 let subcommand_args = get_args_of(
238 parser_of(parent, bin_name).expect(INTERNAL_ERROR_MSG),
239 Some(parent),
240 );
241
242 if !subcommand_args.is_empty() {
243 segments.push(subcommand_args);
244 }
245
246 let children = get_subcommands_of(parser_of(parent, bin_name).expect(INTERNAL_ERROR_MSG));
248
249 if !children.is_empty() {
250 segments.push(children);
251 }
252
253 segments.push(String::from(";;"));
254 all_subcommands.push(segments.join("\n"));
255 }
256
257 let parent_bin_name = parent
258 .get_bin_name()
259 .expect("crate::generate should have set the bin_name");
260
261 format!(
262 "
263 case $state in
264 ({name})
265 words=($line[{pos}] \"${{words[@]}}\")
266 (( CURRENT += 1 ))
267 curcontext=\"${{curcontext%:*:*}}:{name_hyphen}-command-$line[{pos}]:\"
268 case $line[{pos}] in
269 {subcommands}
270 esac
271 ;;
272esac",
273 name = parent.get_name(),
274 name_hyphen = parent_bin_name.replace(' ', "-"),
275 subcommands = all_subcommands.join("\n"),
276 pos = parent.get_positionals().count() + 1
277 )
278}
279
280fn parser_of<'cmd>(parent: &'cmd Command, bin_name: &str) -> Option<&'cmd Command> {
285 debug!("parser_of: p={}, bin_name={}", parent.get_name(), bin_name);
286
287 if bin_name == parent.get_bin_name().unwrap_or_default() {
288 return Some(parent);
289 }
290
291 for subcommand in parent.get_subcommands() {
292 if let Some(ret) = parser_of(subcommand, bin_name) {
293 return Some(ret);
294 }
295 }
296
297 None
298}
299
300fn get_args_of(parent: &Command, p_global: Option<&Command>) -> String {
321 debug!("get_args_of");
322
323 let mut segments = vec![String::from("_arguments \"${_arguments_options[@]}\" : \\")];
324 let opts = write_opts_of(parent, p_global);
325 let flags = write_flags_of(parent, p_global);
326 let positionals = write_positionals_of(parent);
327
328 if !opts.is_empty() {
329 segments.push(opts);
330 }
331
332 if !flags.is_empty() {
333 segments.push(flags);
334 }
335
336 if !positionals.is_empty() {
337 segments.push(positionals);
338 }
339
340 if parent.has_subcommands() {
341 let parent_bin_name = parent
342 .get_bin_name()
343 .expect("crate::generate should have set the bin_name");
344 let subcommand_bin_name = format!(
345 "\":: :_{name}_commands\" \\",
346 name = parent_bin_name.replace(' ', "__")
347 );
348 segments.push(subcommand_bin_name);
349
350 let subcommand_text = format!("\"*::: :->{name}\" \\", name = parent.get_name());
351 segments.push(subcommand_text);
352 };
353
354 segments.push(String::from("&& ret=0"));
355 segments.join("\n")
356}
357
358fn value_completion(arg: &Arg) -> Option<String> {
360 if let Some(values) = utils::possible_values(arg) {
361 if values
362 .iter()
363 .any(|value| !value.is_hide_set() && value.get_help().is_some())
364 {
365 Some(format!(
366 "(({}))",
367 values
368 .iter()
369 .filter_map(|value| {
370 if value.is_hide_set() {
371 None
372 } else {
373 Some(format!(
374 r#"{name}\:"{tooltip}""#,
375 name = escape_value(value.get_name()),
376 tooltip =
377 escape_help(&value.get_help().unwrap_or_default().to_string()),
378 ))
379 }
380 })
381 .collect::<Vec<_>>()
382 .join("\n")
383 ))
384 } else {
385 Some(format!(
386 "({})",
387 values
388 .iter()
389 .filter(|pv| !pv.is_hide_set())
390 .map(|n| n.get_name())
391 .collect::<Vec<_>>()
392 .join(" ")
393 ))
394 }
395 } else {
396 Some(
398 match arg.get_value_hint() {
399 ValueHint::Unknown => "_default",
400 ValueHint::Other => "",
401 ValueHint::AnyPath => "_files",
402 ValueHint::FilePath => "_files",
403 ValueHint::DirPath => "_files -/",
404 ValueHint::ExecutablePath => "_absolute_command_paths",
405 ValueHint::CommandName => "_command_names -e",
406 ValueHint::CommandString => "_cmdstring",
407 ValueHint::CommandWithArguments => "_cmdambivalent",
408 ValueHint::Username => "_users",
409 ValueHint::Hostname => "_hosts",
410 ValueHint::Url => "_urls",
411 ValueHint::EmailAddress => "_email_addresses",
412 _ => {
413 return None;
414 }
415 }
416 .to_string(),
417 )
418 }
419}
420
421fn escape_help(string: &str) -> String {
423 string
424 .replace('\\', "\\\\")
425 .replace('\'', "'\\''")
426 .replace('[', "\\[")
427 .replace(']', "\\]")
428 .replace(':', "\\:")
429 .replace('$', "\\$")
430 .replace('`', "\\`")
431 .replace('\n', " ")
432}
433
434fn 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(' ', "\\ ")
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 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 continue;
624 }
625
626 let cardinality_value;
627 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 super::{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}