clap_complete/aot/shells/
zsh.rs

1use std::io::{Error, 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        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
64// Displays the commands of a subcommand
65// (( $+functions[_[bin_name_underscore]_commands] )) ||
66// _[bin_name_underscore]_commands() {
67//     local commands; commands=(
68//         '[arg_name]:[arg_help]'
69//     )
70//     _describe -t commands '[bin_name] commands' commands "$@"
71//
72// Where the following variables are present:
73//    [bin_name_underscore]: The full space delineated bin_name, where spaces have been replaced by
74//                           underscore characters
75//    [arg_name]: The name of the subcommand
76//    [arg_help]: The help message of the subcommand
77//    [bin_name]: The full space delineated bin_name
78//
79// Here's a snippet from rustup:
80//
81// (( $+functions[_rustup_commands] )) ||
82// _rustup_commands() {
83//     local commands; commands=(
84//      'show:Show the active and installed toolchains'
85//      'update:Update Rust toolchains'
86//      # ... snip for brevity
87//      'help:Print this message or the help of the given subcommand(s)'
88//     )
89//     _describe -t commands 'rustup commands' commands "$@"
90//
91fn 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    // First we do ourself
101    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    // Next we start looping through all the children, grandchildren, etc.
115    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
143// Generates subcommand completions in form of
144//
145//         '[arg_name]:[arg_help]'
146//
147// Where:
148//    [arg_name]: the subcommand's name
149//    [arg_help]: the help message of the subcommand
150//
151// A snippet from rustup:
152//         'show:Show the active and installed toolchains'
153//      'update:Update Rust toolchains'
154fn 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    // The subcommands
172    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    // Surround the text with newlines for proper formatting.
183    // We need this to prevent weirdly formatted `command=(\n        \n)` sections.
184    // When there are no (sub-)commands.
185    if !segments.is_empty() {
186        segments.insert(0, "".to_string());
187        segments.push("    ".to_string());
188    }
189
190    segments.join("\n")
191}
192
193// Get's the subcommand section of a completion file
194// This looks roughly like:
195//
196// case $state in
197// ([bin_name]_args)
198//     curcontext=\"${curcontext%:*:*}:[name_hyphen]-command-$words[1]:\"
199//     case $line[1] in
200//
201//         ([name])
202//         _arguments -C -s -S \
203//             [subcommand_args]
204//         && ret=0
205//
206//         [RECURSIVE_CALLS]
207//
208//         ;;",
209//
210//         [repeat]
211//
212//     esac
213// ;;
214// esac",
215//
216// Where the following variables are present:
217//    [name] = The subcommand name in the form of "install" for "rustup toolchain install"
218//    [bin_name] = The full space delineated bin_name such as "rustup toolchain install"
219//    [name_hyphen] = The full space delineated bin_name, but replace spaces with hyphens
220//    [repeat] = From the same recursive calls, but for all subcommands
221//    [subcommand_args] = The same as zsh::get_args_of
222fn 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 (ref name, ref 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        // Get the help text of all child subcommands.
251        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
284// Get the Command for a given subcommand tree.
285//
286// Given the bin_name "a b c" and the Command for "a" this returns the "c" Command.
287// Given the bin_name "a b c" and the Command for "b" this returns the "c" Command.
288fn 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
304// Writes out the args section, which ends up being the flags, opts and positionals, and a jump to
305// another ZSH function if there are subcommands.
306// The structure works like this:
307//    ([conflicting_args]) [multiple] arg [takes_value] [[help]] [: :(possible_values)]
308//       ^-- list '-v -h'    ^--'*'          ^--'+'                   ^-- list 'one two three'
309//
310// An example from the rustup command:
311//
312// _arguments -C -s -S \
313//         '(-h --help --verbose)-v[Enable verbose output]' \
314//         '(-V -v --version --verbose --help)-h[Print help information]' \
315//      # ... snip for brevity
316//         ':: :_rustup_commands' \    # <-- displays subcommands
317//         '*::: :->rustup' \          # <-- displays subcommand args and child subcommands
318//     && ret=0
319//
320// The args used for _arguments are as follows:
321//    -C: modify the $context internal variable
322//    -s: Allow stacking of short args (i.e. -a -b -c => -abc)
323//    -S: Do not complete anything after '--' and treat those as argument values
324fn 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        // If the command has an external subcommand value parser, we need to
358        // add a catch-all for the subcommand. Otherwise there would be no autocompletion whatsoever.
359        segments.push(String::from("\"*::external_command:_default\" \\"));
360    }
361
362    segments.push(String::from("&& ret=0"));
363    segments.join("\n")
364}
365
366// Uses either `possible_vals` or `value_hint` to give hints about possible argument values
367fn 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        // NB! If you change this, please also update the table in `ValueHint` documentation.
405        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
429/// Escape help string inside single quotes and brackets
430fn 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
442/// Escape value string inside single quotes and parentheses
443fn 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 = vc.repeat(o.get_num_args().expect("built").min_values());
483
484        if let Some(shorts) = o.get_short_and_visible_aliases() {
485            for short in shorts {
486                let s = format!("'{conflicts}{multiple}-{short}+[{help}]{vc}' \\");
487
488                debug!("write_opts_of:iter: Wrote...{}", &*s);
489                ret.push(s);
490            }
491        }
492        if let Some(longs) = o.get_long_and_visible_aliases() {
493            for long in longs {
494                let l = format!("'{conflicts}{multiple}--{long}=[{help}]{vc}' \\");
495
496                debug!("write_opts_of:iter: Wrote...{}", &*l);
497                ret.push(l);
498            }
499        }
500    }
501
502    ret.join("\n")
503}
504
505fn arg_conflicts(cmd: &Command, arg: &Arg, app_global: Option<&Command>) -> String {
506    fn push_conflicts(conflicts: &[&Arg], res: &mut Vec<String>) {
507        for conflict in conflicts {
508            if let Some(s) = conflict.get_short() {
509                res.push(format!("-{s}"));
510            }
511
512            if let Some(l) = conflict.get_long() {
513                res.push(format!("--{l}"));
514            }
515        }
516    }
517
518    let mut res = vec![];
519    match (app_global, arg.is_global_set()) {
520        (Some(x), true) => {
521            let conflicts = x.get_arg_conflicts_with(arg);
522
523            if conflicts.is_empty() {
524                return String::new();
525            }
526
527            push_conflicts(&conflicts, &mut res);
528        }
529        (_, _) => {
530            let conflicts = cmd.get_arg_conflicts_with(arg);
531
532            if conflicts.is_empty() {
533                return String::new();
534            }
535
536            push_conflicts(&conflicts, &mut res);
537        }
538    };
539
540    format!("({})", res.join(" "))
541}
542
543fn write_flags_of(p: &Command, p_global: Option<&Command>) -> String {
544    debug!("write_flags_of;");
545
546    let mut ret = vec![];
547
548    for f in utils::flags(p) {
549        debug!("write_flags_of:iter: f={}", f.get_id());
550
551        let help = escape_help(&f.get_help().unwrap_or_default().to_string());
552        let conflicts = arg_conflicts(p, &f, p_global);
553
554        let multiple = if let ArgAction::Count | ArgAction::Append = f.get_action() {
555            "*"
556        } else {
557            ""
558        };
559
560        if let Some(short) = f.get_short() {
561            let s = format!("'{conflicts}{multiple}-{short}[{help}]' \\");
562
563            debug!("write_flags_of:iter: Wrote...{}", &*s);
564
565            ret.push(s);
566
567            if let Some(short_aliases) = f.get_visible_short_aliases() {
568                for alias in short_aliases {
569                    let s = format!("'{conflicts}{multiple}-{alias}[{help}]' \\",);
570
571                    debug!("write_flags_of:iter: Wrote...{}", &*s);
572
573                    ret.push(s);
574                }
575            }
576        }
577
578        if let Some(long) = f.get_long() {
579            let l = format!("'{conflicts}{multiple}--{long}[{help}]' \\");
580
581            debug!("write_flags_of:iter: Wrote...{}", &*l);
582
583            ret.push(l);
584
585            if let Some(aliases) = f.get_visible_aliases() {
586                for alias in aliases {
587                    let l = format!("'{conflicts}{multiple}--{alias}[{help}]' \\");
588
589                    debug!("write_flags_of:iter: Wrote...{}", &*l);
590
591                    ret.push(l);
592                }
593            }
594        }
595    }
596
597    ret.join("\n")
598}
599
600fn write_positionals_of(p: &Command) -> String {
601    debug!("write_positionals_of;");
602
603    let mut ret = vec![];
604
605    // Completions for commands that end with two Vec arguments require special care.
606    // - You can have two Vec args separated with a custom value terminator.
607    // - You can have two Vec args with the second one set to last (raw sets last)
608    //   which will require a '--' separator to be used before the second argument
609    //   on the command-line.
610    //
611    // We use the '-S' _arguments option to disable completion after '--'. Thus, the
612    // completion for the second argument in scenario (B) does not need to be emitted
613    // because it is implicitly handled by the '-S' option.
614    // We only need to emit the first catch-all.
615    //
616    // Have we already emitted a catch-all multi-valued positional argument
617    // without a custom value terminator?
618    let mut catch_all_emitted = false;
619
620    for arg in p.get_positionals() {
621        debug!("write_positionals_of:iter: arg={}", arg.get_id());
622
623        let num_args = arg.get_num_args().expect("built");
624        let is_multi_valued = num_args.max_values() > 1;
625
626        if catch_all_emitted && (arg.is_last_set() || is_multi_valued) {
627            // This is the final argument and it also takes multiple arguments.
628            // We've already emitted a catch-all positional argument so we don't need
629            // to emit anything for this argument because it is implicitly handled by
630            // the use of the '-S' _arguments option.
631            continue;
632        }
633
634        let cardinality_value;
635        // If we have any subcommands, we'll emit a catch-all argument, so we shouldn't
636        // emit one here.
637        let cardinality = if is_multi_valued && !p.has_subcommands() {
638            match arg.get_value_terminator() {
639                Some(terminator) => {
640                    cardinality_value = format!("*{}:", escape_value(terminator));
641                    cardinality_value.as_str()
642                }
643                None => {
644                    catch_all_emitted = true;
645                    "*:"
646                }
647            }
648        } else if !arg.is_required_set() {
649            ":"
650        } else {
651            ""
652        };
653
654        let a = format!(
655            "'{cardinality}:{name}{help}:{value_completion}' \\",
656            cardinality = cardinality,
657            name = arg.get_id(),
658            help = arg
659                .get_help()
660                .map(|s| s.to_string())
661                .map(|v| " -- ".to_owned() + &v)
662                .unwrap_or_else(|| "".to_owned())
663                .replace('[', "\\[")
664                .replace(']', "\\]")
665                .replace('\'', "'\\''")
666                .replace(':', "\\:"),
667            value_completion = value_completion(arg).unwrap_or_default()
668        );
669
670        debug!("write_positionals_of:iter: Wrote...{a}");
671
672        ret.push(a);
673    }
674
675    ret.join("\n")
676}
677
678#[cfg(test)]
679mod tests {
680    use super::{escape_help, escape_value};
681
682    #[test]
683    fn test_escape_value() {
684        let raw_string = "\\ [foo]() `bar https://$PATH";
685        assert_eq!(
686            escape_value(raw_string),
687            "\\\\\\ \\[foo\\]\\(\\)\\ \\`bar\\ https\\://\\$PATH"
688        );
689    }
690
691    #[test]
692    fn test_escape_help() {
693        let raw_string = "\\ [foo]() `bar https://$PATH";
694        assert_eq!(
695            escape_help(raw_string),
696            "\\\\ \\[foo\\]() \\`bar https\\://\\$PATH"
697        );
698    }
699}