clap_complete/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        w!(
23            buf,
24            format!(
25                "#compdef {name}
26
27autoload -U is-at-least
28
29_{name}() {{
30    typeset -A opt_args
31    typeset -a _arguments_options
32    local ret=1
33
34    if is-at-least 5.2; then
35        _arguments_options=(-s -S -C)
36    else
37        _arguments_options=(-s -C)
38    fi
39
40    local context curcontext=\"$curcontext\" state line
41    {initial_args}{subcommands}
42}}
43
44{subcommand_details}
45
46if [ \"$funcstack[1]\" = \"_{name}\" ]; then
47    _{name} \"$@\"
48else
49    compdef _{name} {name}
50fi
51",
52                name = bin_name,
53                initial_args = get_args_of(cmd, None),
54                subcommands = get_subcommands_of(cmd),
55                subcommand_details = subcommand_details(cmd)
56            )
57            .as_bytes()
58        );
59    }
60}
61
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//
89fn 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_subcommand_bins: Vec<_> = utils::all_subcommands(p)
114        .into_iter()
115        .map(|(_sc_name, bin_name)| bin_name)
116        .collect();
117
118    all_subcommand_bins.sort();
119    all_subcommand_bins.dedup();
120
121    for bin_name in &all_subcommand_bins {
122        debug!("subcommand_details:iter: bin_name={bin_name}");
123
124        ret.push(format!(
125            "\
126(( $+functions[_{bin_name_underscore}_commands] )) ||
127_{bin_name_underscore}_commands() {{
128    local commands; commands=({subcommands_and_args})
129    _describe -t commands '{bin_name} commands' commands \"$@\"
130}}",
131            bin_name_underscore = bin_name.replace(' ', "__"),
132            bin_name = bin_name,
133            subcommands_and_args =
134                subcommands_of(parser_of(p, bin_name).expect(INTERNAL_ERROR_MSG))
135        ));
136    }
137
138    ret.join("\n")
139}
140
141// Generates subcommand completions in form of
142//
143//         '[arg_name]:[arg_help]'
144//
145// Where:
146//    [arg_name]: the subcommand's name
147//    [arg_help]: the help message of the subcommand
148//
149// A snippet from rustup:
150//         'show:Show the active and installed toolchains'
151//      'update:Update Rust toolchains'
152fn subcommands_of(p: &Command) -> String {
153    debug!("subcommands_of");
154
155    let mut segments = vec![];
156
157    fn add_subcommands(subcommand: &Command, name: &str, ret: &mut Vec<String>) {
158        debug!("add_subcommands");
159
160        let text = format!(
161            "'{name}:{help}' \\",
162            name = name,
163            help = escape_help(&subcommand.get_about().unwrap_or_default().to_string())
164        );
165
166        ret.push(text);
167    }
168
169    // The subcommands
170    for command in p.get_subcommands() {
171        debug!("subcommands_of:iter: subcommand={}", command.get_name());
172
173        add_subcommands(command, command.get_name(), &mut segments);
174
175        for alias in command.get_visible_aliases() {
176            add_subcommands(command, alias, &mut segments);
177        }
178    }
179
180    // Surround the text with newlines for proper formatting.
181    // We need this to prevent weirdly formatted `command=(\n        \n)` sections.
182    // When there are no (sub-)commands.
183    if !segments.is_empty() {
184        segments.insert(0, "".to_string());
185        segments.push("    ".to_string());
186    }
187
188    segments.join("\n")
189}
190
191// Get's the subcommand section of a completion file
192// This looks roughly like:
193//
194// case $state in
195// ([bin_name]_args)
196//     curcontext=\"${curcontext%:*:*}:[name_hyphen]-command-$words[1]:\"
197//     case $line[1] in
198//
199//         ([name])
200//         _arguments -C -s -S \
201//             [subcommand_args]
202//         && ret=0
203//
204//         [RECURSIVE_CALLS]
205//
206//         ;;",
207//
208//         [repeat]
209//
210//     esac
211// ;;
212// esac",
213//
214// Where the following variables are present:
215//    [name] = The subcommand name in the form of "install" for "rustup toolchain install"
216//    [bin_name] = The full space delineated bin_name such as "rustup toolchain install"
217//    [name_hyphen] = The full space delineated bin_name, but replace spaces with hyphens
218//    [repeat] = From the same recursive calls, but for all subcommands
219//    [subcommand_args] = The same as zsh::get_args_of
220fn get_subcommands_of(parent: &Command) -> String {
221    debug!(
222        "get_subcommands_of: Has subcommands...{:?}",
223        parent.has_subcommands()
224    );
225
226    if !parent.has_subcommands() {
227        return String::new();
228    }
229
230    let subcommand_names = utils::subcommands(parent);
231    let mut all_subcommands = vec![];
232
233    for (ref name, ref bin_name) in &subcommand_names {
234        debug!(
235            "get_subcommands_of:iter: parent={}, name={name}, bin_name={bin_name}",
236            parent.get_name(),
237        );
238        let mut segments = vec![format!("({name})")];
239        let subcommand_args = get_args_of(
240            parser_of(parent, bin_name).expect(INTERNAL_ERROR_MSG),
241            Some(parent),
242        );
243
244        if !subcommand_args.is_empty() {
245            segments.push(subcommand_args);
246        }
247
248        // Get the help text of all child subcommands.
249        let children = get_subcommands_of(parser_of(parent, bin_name).expect(INTERNAL_ERROR_MSG));
250
251        if !children.is_empty() {
252            segments.push(children);
253        }
254
255        segments.push(String::from(";;"));
256        all_subcommands.push(segments.join("\n"));
257    }
258
259    let parent_bin_name = parent
260        .get_bin_name()
261        .expect("crate::generate should have set the bin_name");
262
263    format!(
264        "
265    case $state in
266    ({name})
267        words=($line[{pos}] \"${{words[@]}}\")
268        (( CURRENT += 1 ))
269        curcontext=\"${{curcontext%:*:*}}:{name_hyphen}-command-$line[{pos}]:\"
270        case $line[{pos}] in
271            {subcommands}
272        esac
273    ;;
274esac",
275        name = parent.get_name(),
276        name_hyphen = parent_bin_name.replace(' ', "-"),
277        subcommands = all_subcommands.join("\n"),
278        pos = parent.get_positionals().count() + 1
279    )
280}
281
282// Get the Command for a given subcommand tree.
283//
284// Given the bin_name "a b c" and the Command for "a" this returns the "c" Command.
285// Given the bin_name "a b c" and the Command for "b" this returns the "c" Command.
286fn parser_of<'cmd>(parent: &'cmd Command, bin_name: &str) -> Option<&'cmd Command> {
287    debug!("parser_of: p={}, bin_name={}", parent.get_name(), bin_name);
288
289    if bin_name == parent.get_bin_name().unwrap_or_default() {
290        return Some(parent);
291    }
292
293    for subcommand in parent.get_subcommands() {
294        if let Some(ret) = parser_of(subcommand, bin_name) {
295            return Some(ret);
296        }
297    }
298
299    None
300}
301
302// Writes out the args section, which ends up being the flags, opts and positionals, and a jump to
303// another ZSH function if there are subcommands.
304// The structure works like this:
305//    ([conflicting_args]) [multiple] arg [takes_value] [[help]] [: :(possible_values)]
306//       ^-- list '-v -h'    ^--'*'          ^--'+'                   ^-- list 'one two three'
307//
308// An example from the rustup command:
309//
310// _arguments -C -s -S \
311//         '(-h --help --verbose)-v[Enable verbose output]' \
312//         '(-V -v --version --verbose --help)-h[Print help information]' \
313//      # ... snip for brevity
314//         ':: :_rustup_commands' \    # <-- displays subcommands
315//         '*::: :->rustup' \          # <-- displays subcommand args and child subcommands
316//     && ret=0
317//
318// The args used for _arguments are as follows:
319//    -C: modify the $context internal variable
320//    -s: Allow stacking of short args (i.e. -a -b -c => -abc)
321//    -S: Do not complete anything after '--' and treat those as argument values
322fn get_args_of(parent: &Command, p_global: Option<&Command>) -> String {
323    debug!("get_args_of");
324
325    let mut segments = vec![String::from("_arguments \"${_arguments_options[@]}\" : \\")];
326    let opts = write_opts_of(parent, p_global);
327    let flags = write_flags_of(parent, p_global);
328    let positionals = write_positionals_of(parent);
329
330    if !opts.is_empty() {
331        segments.push(opts);
332    }
333
334    if !flags.is_empty() {
335        segments.push(flags);
336    }
337
338    if !positionals.is_empty() {
339        segments.push(positionals);
340    }
341
342    if parent.has_subcommands() {
343        let parent_bin_name = parent
344            .get_bin_name()
345            .expect("crate::generate should have set the bin_name");
346        let subcommand_bin_name = format!(
347            "\":: :_{name}_commands\" \\",
348            name = parent_bin_name.replace(' ', "__")
349        );
350        segments.push(subcommand_bin_name);
351
352        let subcommand_text = format!("\"*::: :->{name}\" \\", name = parent.get_name());
353        segments.push(subcommand_text);
354    };
355
356    segments.push(String::from("&& ret=0"));
357    segments.join("\n")
358}
359
360// Uses either `possible_vals` or `value_hint` to give hints about possible argument values
361fn value_completion(arg: &Arg) -> Option<String> {
362    if let Some(values) = utils::possible_values(arg) {
363        if values
364            .iter()
365            .any(|value| !value.is_hide_set() && value.get_help().is_some())
366        {
367            Some(format!(
368                "(({}))",
369                values
370                    .iter()
371                    .filter_map(|value| {
372                        if value.is_hide_set() {
373                            None
374                        } else {
375                            Some(format!(
376                                r#"{name}\:"{tooltip}""#,
377                                name = escape_value(value.get_name()),
378                                tooltip =
379                                    escape_help(&value.get_help().unwrap_or_default().to_string()),
380                            ))
381                        }
382                    })
383                    .collect::<Vec<_>>()
384                    .join("\n")
385            ))
386        } else {
387            Some(format!(
388                "({})",
389                values
390                    .iter()
391                    .filter(|pv| !pv.is_hide_set())
392                    .map(|n| n.get_name())
393                    .collect::<Vec<_>>()
394                    .join(" ")
395            ))
396        }
397    } else {
398        // NB! If you change this, please also update the table in `ValueHint` documentation.
399        Some(
400            match arg.get_value_hint() {
401                ValueHint::Unknown => {
402                    return None;
403                }
404                ValueHint::Other => "( )",
405                ValueHint::AnyPath => "_files",
406                ValueHint::FilePath => "_files",
407                ValueHint::DirPath => "_files -/",
408                ValueHint::ExecutablePath => "_absolute_command_paths",
409                ValueHint::CommandName => "_command_names -e",
410                ValueHint::CommandString => "_cmdstring",
411                ValueHint::CommandWithArguments => "_cmdambivalent",
412                ValueHint::Username => "_users",
413                ValueHint::Hostname => "_hosts",
414                ValueHint::Url => "_urls",
415                ValueHint::EmailAddress => "_email_addresses",
416                _ => {
417                    return None;
418                }
419            }
420            .to_string(),
421        )
422    }
423}
424
425/// Escape help string inside single quotes and brackets
426fn escape_help(string: &str) -> String {
427    string
428        .replace('\\', "\\\\")
429        .replace('\'', "'\\''")
430        .replace('[', "\\[")
431        .replace(']', "\\]")
432        .replace(':', "\\:")
433        .replace('$', "\\$")
434        .replace('`', "\\`")
435        .replace('\n', " ")
436}
437
438/// Escape value string inside single quotes and parentheses
439fn escape_value(string: &str) -> String {
440    string
441        .replace('\\', "\\\\")
442        .replace('\'', "'\\''")
443        .replace('[', "\\[")
444        .replace(']', "\\]")
445        .replace(':', "\\:")
446        .replace('$', "\\$")
447        .replace('`', "\\`")
448        .replace('(', "\\(")
449        .replace(')', "\\)")
450        .replace(' ', "\\ ")
451}
452
453fn write_opts_of(p: &Command, p_global: Option<&Command>) -> String {
454    debug!("write_opts_of");
455
456    let mut ret = vec![];
457
458    for o in p.get_opts() {
459        debug!("write_opts_of:iter: o={}", o.get_id());
460
461        let help = escape_help(&o.get_help().unwrap_or_default().to_string());
462        let conflicts = arg_conflicts(p, o, p_global);
463
464        let multiple = if let ArgAction::Count | ArgAction::Append = o.get_action() {
465            "*"
466        } else {
467            ""
468        };
469
470        let vn = match o.get_value_names() {
471            None => " ".to_string(),
472            Some(val) => val[0].to_string(),
473        };
474        let vc = match value_completion(o) {
475            Some(val) => format!(":{vn}:{val}"),
476            None => format!(":{vn}: "),
477        };
478        let vc = vc.repeat(o.get_num_args().expect("built").min_values());
479
480        if let Some(shorts) = o.get_short_and_visible_aliases() {
481            for short in shorts {
482                let s = format!("'{conflicts}{multiple}-{short}+[{help}]{vc}' \\");
483
484                debug!("write_opts_of:iter: Wrote...{}", &*s);
485                ret.push(s);
486            }
487        }
488        if let Some(longs) = o.get_long_and_visible_aliases() {
489            for long in longs {
490                let l = format!("'{conflicts}{multiple}--{long}=[{help}]{vc}' \\");
491
492                debug!("write_opts_of:iter: Wrote...{}", &*l);
493                ret.push(l);
494            }
495        }
496    }
497
498    ret.join("\n")
499}
500
501fn arg_conflicts(cmd: &Command, arg: &Arg, app_global: Option<&Command>) -> String {
502    fn push_conflicts(conflicts: &[&Arg], res: &mut Vec<String>) {
503        for conflict in conflicts {
504            if let Some(s) = conflict.get_short() {
505                res.push(format!("-{s}"));
506            }
507
508            if let Some(l) = conflict.get_long() {
509                res.push(format!("--{l}"));
510            }
511        }
512    }
513
514    let mut res = vec![];
515    match (app_global, arg.is_global_set()) {
516        (Some(x), true) => {
517            let conflicts = x.get_arg_conflicts_with(arg);
518
519            if conflicts.is_empty() {
520                return String::new();
521            }
522
523            push_conflicts(&conflicts, &mut res);
524        }
525        (_, _) => {
526            let conflicts = cmd.get_arg_conflicts_with(arg);
527
528            if conflicts.is_empty() {
529                return String::new();
530            }
531
532            push_conflicts(&conflicts, &mut res);
533        }
534    };
535
536    format!("({})", res.join(" "))
537}
538
539fn write_flags_of(p: &Command, p_global: Option<&Command>) -> String {
540    debug!("write_flags_of;");
541
542    let mut ret = vec![];
543
544    for f in utils::flags(p) {
545        debug!("write_flags_of:iter: f={}", f.get_id());
546
547        let help = escape_help(&f.get_help().unwrap_or_default().to_string());
548        let conflicts = arg_conflicts(p, &f, p_global);
549
550        let multiple = if let ArgAction::Count | ArgAction::Append = f.get_action() {
551            "*"
552        } else {
553            ""
554        };
555
556        if let Some(short) = f.get_short() {
557            let s = format!("'{conflicts}{multiple}-{short}[{help}]' \\");
558
559            debug!("write_flags_of:iter: Wrote...{}", &*s);
560
561            ret.push(s);
562
563            if let Some(short_aliases) = f.get_visible_short_aliases() {
564                for alias in short_aliases {
565                    let s = format!("'{conflicts}{multiple}-{alias}[{help}]' \\",);
566
567                    debug!("write_flags_of:iter: Wrote...{}", &*s);
568
569                    ret.push(s);
570                }
571            }
572        }
573
574        if let Some(long) = f.get_long() {
575            let l = format!("'{conflicts}{multiple}--{long}[{help}]' \\");
576
577            debug!("write_flags_of:iter: Wrote...{}", &*l);
578
579            ret.push(l);
580
581            if let Some(aliases) = f.get_visible_aliases() {
582                for alias in aliases {
583                    let l = format!("'{conflicts}{multiple}--{alias}[{help}]' \\");
584
585                    debug!("write_flags_of:iter: Wrote...{}", &*l);
586
587                    ret.push(l);
588                }
589            }
590        }
591    }
592
593    ret.join("\n")
594}
595
596fn write_positionals_of(p: &Command) -> String {
597    debug!("write_positionals_of;");
598
599    let mut ret = vec![];
600
601    // Completions for commands that end with two Vec arguments require special care.
602    // - You can have two Vec args separated with a custom value terminator.
603    // - You can have two Vec args with the second one set to last (raw sets last)
604    //   which will require a '--' separator to be used before the second argument
605    //   on the command-line.
606    //
607    // We use the '-S' _arguments option to disable completion after '--'. Thus, the
608    // completion for the second argument in scenario (B) does not need to be emitted
609    // because it is implicitly handled by the '-S' option.
610    // We only need to emit the first catch-all.
611    //
612    // Have we already emitted a catch-all multi-valued positional argument
613    // without a custom value terminator?
614    let mut catch_all_emitted = false;
615
616    for arg in p.get_positionals() {
617        debug!("write_positionals_of:iter: arg={}", arg.get_id());
618
619        let num_args = arg.get_num_args().expect("built");
620        let is_multi_valued = num_args.max_values() > 1;
621
622        if catch_all_emitted && (arg.is_last_set() || is_multi_valued) {
623            // This is the final argument and it also takes multiple arguments.
624            // We've already emitted a catch-all positional argument so we don't need
625            // to emit anything for this argument because it is implicitly handled by
626            // the use of the '-S' _arguments option.
627            continue;
628        }
629
630        let cardinality_value;
631        // If we have any subcommands, we'll emit a catch-all argument, so we shouldn't
632        // emit one here.
633        let cardinality = if is_multi_valued && !p.has_subcommands() {
634            match arg.get_value_terminator() {
635                Some(terminator) => {
636                    cardinality_value = format!("*{}:", escape_value(terminator));
637                    cardinality_value.as_str()
638                }
639                None => {
640                    catch_all_emitted = true;
641                    "*:"
642                }
643            }
644        } else if !arg.is_required_set() {
645            ":"
646        } else {
647            ""
648        };
649
650        let a = format!(
651            "'{cardinality}:{name}{help}:{value_completion}' \\",
652            cardinality = cardinality,
653            name = arg.get_id(),
654            help = arg
655                .get_help()
656                .map(|s| s.to_string())
657                .map(|v| " -- ".to_owned() + &v)
658                .unwrap_or_else(|| "".to_owned())
659                .replace('[', "\\[")
660                .replace(']', "\\]")
661                .replace('\'', "'\\''")
662                .replace(':', "\\:"),
663            value_completion = value_completion(arg).unwrap_or_default()
664        );
665
666        debug!("write_positionals_of:iter: Wrote...{a}");
667
668        ret.push(a);
669    }
670
671    ret.join("\n")
672}
673
674#[cfg(test)]
675mod tests {
676    use crate::shells::zsh::{escape_help, escape_value};
677
678    #[test]
679    fn test_escape_value() {
680        let raw_string = "\\ [foo]() `bar https://$PATH";
681        assert_eq!(
682            escape_value(raw_string),
683            "\\\\\\ \\[foo\\]\\(\\)\\ \\`bar\\ https\\://\\$PATH"
684        );
685    }
686
687    #[test]
688    fn test_escape_help() {
689        let raw_string = "\\ [foo]() `bar https://$PATH";
690        assert_eq!(
691            escape_help(raw_string),
692            "\\\\ \\[foo\\]() \\`bar https\\://\\$PATH"
693        );
694    }
695}