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