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