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/// Generate zsh completion file
9#[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
60// Displays the commands of a subcommand
61// (( $+functions[_[bin_name_underscore]_commands] )) ||
62// _[bin_name_underscore]_commands() {
63//     local commands; commands=(
64//         '[arg_name]:[arg_help]'
65//     )
66//     _describe -t commands '[bin_name] commands' commands "$@"
67//
68// Where the following variables are present:
69//    [bin_name_underscore]: The full space delineated bin_name, where spaces have been replaced by
70//                           underscore characters
71//    [arg_name]: The name of the subcommand
72//    [arg_help]: The help message of the subcommand
73//    [bin_name]: The full space delineated bin_name
74//
75// Here's a snippet from rustup:
76//
77// (( $+functions[_rustup_commands] )) ||
78// _rustup_commands() {
79//     local commands; commands=(
80//      'show:Show the active and installed toolchains'
81//      'update:Update Rust toolchains'
82//      # ... snip for brevity
83//      'help:Print this message or the help of the given subcommand(s)'
84//     )
85//     _describe -t commands 'rustup commands' commands "$@"
86//
87fn 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    // First we do ourself
97    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    // Next we start looping through all the children, grandchildren, etc.
111    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
139// Generates subcommand completions in form of
140//
141//         '[arg_name]:[arg_help]'
142//
143// Where:
144//    [arg_name]: the subcommand's name
145//    [arg_help]: the help message of the subcommand
146//
147// A snippet from rustup:
148//         'show:Show the active and installed toolchains'
149//      'update:Update Rust toolchains'
150fn 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    // The subcommands
168    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    // Surround the text with newlines for proper formatting.
179    // We need this to prevent weirdly formatted `command=(\n        \n)` sections.
180    // When there are no (sub-)commands.
181    if !segments.is_empty() {
182        segments.insert(0, "".to_string());
183        segments.push("    ".to_string());
184    }
185
186    segments.join("\n")
187}
188
189// Get's the subcommand section of a completion file
190// This looks roughly like:
191//
192// case $state in
193// ([bin_name]_args)
194//     curcontext=\"${curcontext%:*:*}:[name_hyphen]-command-$words[1]:\"
195//     case $line[1] in
196//
197//         ([name])
198//         _arguments -C -s -S \
199//             [subcommand_args]
200//         && ret=0
201//
202//         [RECURSIVE_CALLS]
203//
204//         ;;",
205//
206//         [repeat]
207//
208//     esac
209// ;;
210// esac",
211//
212// Where the following variables are present:
213//    [name] = The subcommand name in the form of "install" for "rustup toolchain install"
214//    [bin_name] = The full space delineated bin_name such as "rustup toolchain install"
215//    [name_hyphen] = The full space delineated bin_name, but replace spaces with hyphens
216//    [repeat] = From the same recursive calls, but for all subcommands
217//    [subcommand_args] = The same as zsh::get_args_of
218fn 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        // Get the help text of all child subcommands.
247        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
280// Get the Command for a given subcommand tree.
281//
282// Given the bin_name "a b c" and the Command for "a" this returns the "c" Command.
283// Given the bin_name "a b c" and the Command for "b" this returns the "c" Command.
284fn 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
300// Writes out the args section, which ends up being the flags, opts and positionals, and a jump to
301// another ZSH function if there are subcommands.
302// The structure works like this:
303//    ([conflicting_args]) [multiple] arg [takes_value] [[help]] [: :(possible_values)]
304//       ^-- list '-v -h'    ^--'*'          ^--'+'                   ^-- list 'one two three'
305//
306// An example from the rustup command:
307//
308// _arguments -C -s -S \
309//         '(-h --help --verbose)-v[Enable verbose output]' \
310//         '(-V -v --version --verbose --help)-h[Print help information]' \
311//      # ... snip for brevity
312//         ':: :_rustup_commands' \    # <-- displays subcommands
313//         '*::: :->rustup' \          # <-- displays subcommand args and child subcommands
314//     && ret=0
315//
316// The args used for _arguments are as follows:
317//    -C: modify the $context internal variable
318//    -s: Allow stacking of short args (i.e. -a -b -c => -abc)
319//    -S: Do not complete anything after '--' and treat those as argument values
320fn 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
358// Uses either `possible_vals` or `value_hint` to give hints about possible argument values
359fn 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        // NB! If you change this, please also update the table in `ValueHint` documentation.
397        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
421/// Escape help string inside single quotes and brackets
422fn 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
434/// Escape value string inside single quotes and parentheses
435fn 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    // Completions for commands that end with two Vec arguments require special care.
598    // - You can have two Vec args separated with a custom value terminator.
599    // - You can have two Vec args with the second one set to last (raw sets last)
600    //   which will require a '--' separator to be used before the second argument
601    //   on the command-line.
602    //
603    // We use the '-S' _arguments option to disable completion after '--'. Thus, the
604    // completion for the second argument in scenario (B) does not need to be emitted
605    // because it is implicitly handled by the '-S' option.
606    // We only need to emit the first catch-all.
607    //
608    // Have we already emitted a catch-all multi-valued positional argument
609    // without a custom value terminator?
610    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            // This is the final argument and it also takes multiple arguments.
620            // We've already emitted a catch-all positional argument so we don't need
621            // to emit anything for this argument because it is implicitly handled by
622            // the use of the '-S' _arguments option.
623            continue;
624        }
625
626        let cardinality_value;
627        // If we have any subcommands, we'll emit a catch-all argument, so we shouldn't
628        // emit one here.
629        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}