clap_complete/aot/shells/
zsh.rs1use std::io::{Error, Write};
2
3use clap::{Arg, ArgAction, Command, ValueHint};
4
5use crate::INTERNAL_ERROR_MSG;
6use crate::generator::{Generator, utils};
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 (name, 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 = match o.get_num_args().expect("built").min_values() {
483 0 => format!(":{vc}"),
484 min_value => vc.repeat(min_value),
485 };
486
487 if let Some(shorts) = o.get_short_and_visible_aliases() {
488 for short in shorts {
489 let s = format!("'{conflicts}{multiple}-{short}+[{help}]{vc}' \\");
490
491 debug!("write_opts_of:iter: Wrote...{}", &*s);
492 ret.push(s);
493 }
494 }
495 if let Some(longs) = o.get_long_and_visible_aliases() {
496 for long in longs {
497 let l = format!("'{conflicts}{multiple}--{long}=[{help}]{vc}' \\");
498
499 debug!("write_opts_of:iter: Wrote...{}", &*l);
500 ret.push(l);
501 }
502 }
503 }
504
505 ret.join("\n")
506}
507
508fn arg_conflicts(cmd: &Command, arg: &Arg, app_global: Option<&Command>) -> String {
509 fn push_conflicts(conflicts: &[&Arg], res: &mut Vec<String>) {
510 for conflict in conflicts {
511 if let Some(s) = conflict.get_short() {
512 res.push(format!("-{s}"));
513 }
514
515 if let Some(l) = conflict.get_long() {
516 res.push(format!("--{l}"));
517 }
518 }
519 }
520
521 let mut res = vec![];
522 match (app_global, arg.is_global_set()) {
523 (Some(x), true) => {
524 let conflicts = x.get_arg_conflicts_with(arg);
525
526 if conflicts.is_empty() {
527 return String::new();
528 }
529
530 push_conflicts(&conflicts, &mut res);
531 }
532 (_, _) => {
533 let conflicts = cmd.get_arg_conflicts_with(arg);
534
535 if conflicts.is_empty() {
536 return String::new();
537 }
538
539 push_conflicts(&conflicts, &mut res);
540 }
541 };
542
543 format!("({})", res.join(" "))
544}
545
546fn write_flags_of(p: &Command, p_global: Option<&Command>) -> String {
547 debug!("write_flags_of;");
548
549 let mut ret = vec![];
550
551 for f in utils::flags(p) {
552 debug!("write_flags_of:iter: f={}", f.get_id());
553
554 let help = escape_help(&f.get_help().unwrap_or_default().to_string());
555 let conflicts = arg_conflicts(p, &f, p_global);
556
557 let multiple = if let ArgAction::Count | ArgAction::Append = f.get_action() {
558 "*"
559 } else {
560 ""
561 };
562
563 if let Some(short) = f.get_short() {
564 let s = format!("'{conflicts}{multiple}-{short}[{help}]' \\");
565
566 debug!("write_flags_of:iter: Wrote...{}", &*s);
567
568 ret.push(s);
569
570 if let Some(short_aliases) = f.get_visible_short_aliases() {
571 for alias in short_aliases {
572 let s = format!("'{conflicts}{multiple}-{alias}[{help}]' \\",);
573
574 debug!("write_flags_of:iter: Wrote...{}", &*s);
575
576 ret.push(s);
577 }
578 }
579 }
580
581 if let Some(long) = f.get_long() {
582 let l = format!("'{conflicts}{multiple}--{long}[{help}]' \\");
583
584 debug!("write_flags_of:iter: Wrote...{}", &*l);
585
586 ret.push(l);
587
588 if let Some(aliases) = f.get_visible_aliases() {
589 for alias in aliases {
590 let l = format!("'{conflicts}{multiple}--{alias}[{help}]' \\");
591
592 debug!("write_flags_of:iter: Wrote...{}", &*l);
593
594 ret.push(l);
595 }
596 }
597 }
598 }
599
600 ret.join("\n")
601}
602
603fn write_positionals_of(p: &Command) -> String {
604 debug!("write_positionals_of;");
605
606 let mut ret = vec![];
607
608 let mut catch_all_emitted = false;
622
623 for arg in p.get_positionals() {
624 debug!("write_positionals_of:iter: arg={}", arg.get_id());
625
626 let num_args = arg.get_num_args().expect("built");
627 let is_multi_valued = num_args.max_values() > 1;
628
629 if catch_all_emitted && (arg.is_last_set() || is_multi_valued) {
630 continue;
635 }
636
637 let cardinality_value;
638 let cardinality = if is_multi_valued && !p.has_subcommands() {
641 match arg.get_value_terminator() {
642 Some(terminator) => {
643 cardinality_value = format!("*{}:", escape_value(terminator));
644 cardinality_value.as_str()
645 }
646 None => {
647 catch_all_emitted = true;
648 "*:"
649 }
650 }
651 } else if !arg.is_required_set() {
652 ":"
653 } else {
654 ""
655 };
656
657 let a = format!(
658 "'{cardinality}:{name}{help}:{value_completion}' \\",
659 cardinality = cardinality,
660 name = arg.get_id(),
661 help = arg
662 .get_help()
663 .map(|s| s.to_string())
664 .map(|v| " -- ".to_owned() + &v)
665 .unwrap_or_else(|| "".to_owned())
666 .replace('[', "\\[")
667 .replace(']', "\\]")
668 .replace('\'', "'\\''")
669 .replace(':', "\\:"),
670 value_completion = value_completion(arg).unwrap_or_default()
671 );
672
673 debug!("write_positionals_of:iter: Wrote...{a}");
674
675 ret.push(a);
676 }
677
678 ret.join("\n")
679}
680
681#[cfg(test)]
682mod tests {
683 use super::{escape_help, escape_value};
684
685 #[test]
686 fn test_escape_value() {
687 let raw_string = "\\ [foo]() `bar https://$PATH";
688 assert_eq!(
689 escape_value(raw_string),
690 "\\\\\\ \\[foo\\]\\(\\)\\ \\`bar\\ https\\://\\$PATH"
691 );
692 }
693
694 #[test]
695 fn test_escape_help() {
696 let raw_string = "\\ [foo]() `bar https://$PATH";
697 assert_eq!(
698 escape_help(raw_string),
699 "\\\\ \\[foo\\]() \\`bar https\\://\\$PATH"
700 );
701 }
702}