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