clap/completions/
zsh.rs

1// Std
2#[allow(deprecated, unused_imports)]
3use std::{ascii::AsciiExt, io::Write};
4
5// Internal
6use crate::{
7    app::{parser::Parser, App},
8    args::{AnyArg, ArgSettings},
9    completions, INTERNAL_ERROR_MSG,
10};
11
12pub struct ZshGen<'a, 'b>
13where
14    'a: 'b,
15{
16    p: &'b Parser<'a, 'b>,
17}
18
19impl<'a, 'b> ZshGen<'a, 'b> {
20    pub fn new(p: &'b Parser<'a, 'b>) -> Self {
21        debugln!("ZshGen::new;");
22        ZshGen { p }
23    }
24
25    pub fn generate_to<W: Write>(&self, buf: &mut W) {
26        debugln!("ZshGen::generate_to;");
27        w!(
28            buf,
29            format!(
30                "\
31#compdef {name}
32
33autoload -U is-at-least
34
35_{name}() {{
36    typeset -A opt_args
37    typeset -a _arguments_options
38    local ret=1
39
40    if is-at-least 5.2; then
41        _arguments_options=(-s -S -C)
42    else
43        _arguments_options=(-s -C)
44    fi
45
46    local context curcontext=\"$curcontext\" state line
47    {initial_args}
48    {subcommands}
49}}
50
51{subcommand_details}
52
53_{name} \"$@\"",
54                name = self.p.meta.bin_name.as_ref().unwrap(),
55                initial_args = get_args_of(self.p),
56                subcommands = get_subcommands_of(self.p),
57                subcommand_details = subcommand_details(self.p)
58            )
59            .as_bytes()
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:Prints this message or the help of the given subcommand(s)'
88// 	)
89// 	_describe -t commands 'rustup commands' commands "$@"
90//
91fn subcommand_details(p: &Parser) -> String {
92    debugln!("ZshGen::subcommand_details;");
93    // First we do ourself
94    let mut ret = vec![format!(
95        "\
96(( $+functions[_{bin_name_underscore}_commands] )) ||
97_{bin_name_underscore}_commands() {{
98    local commands; commands=(
99        {subcommands_and_args}
100    )
101    _describe -t commands '{bin_name} commands' commands \"$@\"
102}}",
103        bin_name_underscore = p.meta.bin_name.as_ref().unwrap().replace(" ", "__"),
104        bin_name = p.meta.bin_name.as_ref().unwrap(),
105        subcommands_and_args = subcommands_of(p)
106    )];
107
108    // Next we start looping through all the children, grandchildren, etc.
109    let mut all_subcommands = completions::all_subcommands(p);
110    all_subcommands.sort();
111    all_subcommands.dedup();
112    for &(_, ref bin_name) in &all_subcommands {
113        debugln!("ZshGen::subcommand_details:iter: bin_name={}", bin_name);
114        ret.push(format!(
115            "\
116(( $+functions[_{bin_name_underscore}_commands] )) ||
117_{bin_name_underscore}_commands() {{
118    local commands; commands=(
119        {subcommands_and_args}
120    )
121    _describe -t commands '{bin_name} commands' commands \"$@\"
122}}",
123            bin_name_underscore = bin_name.replace(" ", "__"),
124            bin_name = bin_name,
125            subcommands_and_args = subcommands_of(parser_of(p, bin_name))
126        ));
127    }
128
129    ret.join("\n")
130}
131
132// Generates subcommand completions in form of
133//
134// 		'[arg_name]:[arg_help]'
135//
136// Where:
137//    [arg_name]: the subcommand's name
138//    [arg_help]: the help message of the subcommand
139//
140// A snippet from rustup:
141// 		'show:Show the active and installed toolchains'
142//      'update:Update Rust toolchains'
143fn subcommands_of(p: &Parser) -> String {
144    debugln!("ZshGen::subcommands_of;");
145    let mut ret = vec![];
146    fn add_sc(sc: &App, n: &str, ret: &mut Vec<String>) {
147        debugln!("ZshGen::add_sc;");
148        let s = format!(
149            "\"{name}:{help}\" \\",
150            name = n,
151            help =
152                sc.p.meta
153                    .about
154                    .unwrap_or("")
155                    .replace("[", "\\[")
156                    .replace("]", "\\]")
157        );
158        if !s.is_empty() {
159            ret.push(s);
160        }
161    }
162
163    // The subcommands
164    for sc in p.subcommands() {
165        debugln!("ZshGen::subcommands_of:iter: subcommand={}", sc.p.meta.name);
166        add_sc(sc, &sc.p.meta.name, &mut ret);
167        if let Some(ref v) = sc.p.meta.aliases {
168            for alias in v.iter().filter(|&&(_, vis)| vis).map(|&(n, _)| n) {
169                add_sc(sc, alias, &mut ret);
170            }
171        }
172    }
173
174    ret.join("\n")
175}
176
177// Get's the subcommand section of a completion file
178// This looks roughly like:
179//
180// case $state in
181// ([bin_name]_args)
182//     curcontext=\"${curcontext%:*:*}:[name_hyphen]-command-$words[1]:\"
183//     case $line[1] in
184//
185//         ([name])
186//         _arguments -C -s -S \
187//             [subcommand_args]
188//         && ret=0
189//
190//         [RECURSIVE_CALLS]
191//
192//         ;;",
193//
194//         [repeat]
195//
196//     esac
197// ;;
198// esac",
199//
200// Where the following variables are present:
201//    [name] = The subcommand name in the form of "install" for "rustup toolchain install"
202//    [bin_name] = The full space delineated bin_name such as "rustup toolchain install"
203//    [name_hyphen] = The full space delineated bin_name, but replace spaces with hyphens
204//    [repeat] = From the same recursive calls, but for all subcommands
205//    [subcommand_args] = The same as zsh::get_args_of
206fn get_subcommands_of(p: &Parser) -> String {
207    debugln!("get_subcommands_of;");
208
209    debugln!(
210        "get_subcommands_of: Has subcommands...{:?}",
211        p.has_subcommands()
212    );
213    if !p.has_subcommands() {
214        return String::new();
215    }
216
217    let sc_names = completions::subcommands_of(p);
218
219    let mut subcmds = vec![];
220    for &(ref name, ref bin_name) in &sc_names {
221        let mut v = vec![format!("({})", name)];
222        let subcommand_args = get_args_of(parser_of(p, &*bin_name));
223        if !subcommand_args.is_empty() {
224            v.push(subcommand_args);
225        }
226        let subcommands = get_subcommands_of(parser_of(p, &*bin_name));
227        if !subcommands.is_empty() {
228            v.push(subcommands);
229        }
230        v.push(String::from(";;"));
231        subcmds.push(v.join("\n"));
232    }
233
234    format!(
235        "case $state in
236    ({name})
237        words=($line[{pos}] \"${{words[@]}}\")
238        (( CURRENT += 1 ))
239        curcontext=\"${{curcontext%:*:*}}:{name_hyphen}-command-$line[{pos}]:\"
240        case $line[{pos}] in
241            {subcommands}
242        esac
243    ;;
244esac",
245        name = p.meta.name,
246        name_hyphen = p.meta.bin_name.as_ref().unwrap().replace(" ", "-"),
247        subcommands = subcmds.join("\n"),
248        pos = p.positionals().len() + 1
249    )
250}
251
252fn parser_of<'a, 'b>(p: &'b Parser<'a, 'b>, sc: &str) -> &'b Parser<'a, 'b> {
253    debugln!("parser_of: sc={}", sc);
254    if sc == p.meta.bin_name.as_ref().unwrap_or(&String::new()) {
255        return p;
256    }
257    &p.find_subcommand(sc).expect(INTERNAL_ERROR_MSG).p
258}
259
260// Writes out the args section, which ends up being the flags, opts and postionals, and a jump to
261// another ZSH function if there are subcommands.
262// The structer works like this:
263//    ([conflicting_args]) [multiple] arg [takes_value] [[help]] [: :(possible_values)]
264//       ^-- list '-v -h'    ^--'*'          ^--'+'                   ^-- list 'one two three'
265//
266// An example from the rustup command:
267//
268// _arguments -C -s -S \
269// 		'(-h --help --verbose)-v[Enable verbose output]' \
270// 		'(-V -v --version --verbose --help)-h[Prints help information]' \
271//      # ... snip for brevity
272// 		':: :_rustup_commands' \    # <-- displays subcommands
273// 		'*::: :->rustup' \          # <-- displays subcommand args and child subcommands
274// 	&& ret=0
275//
276// The args used for _arguments are as follows:
277//    -C: modify the $context internal variable
278//    -s: Allow stacking of short args (i.e. -a -b -c => -abc)
279//    -S: Do not complete anything after '--' and treat those as argument values
280fn get_args_of(p: &Parser) -> String {
281    debugln!("get_args_of;");
282    let mut ret = vec![String::from("_arguments \"${_arguments_options[@]}\" \\")];
283    let opts = write_opts_of(p);
284    let flags = write_flags_of(p);
285    let positionals = write_positionals_of(p);
286    let sc_or_a = if p.has_subcommands() {
287        format!(
288            "\":: :_{name}_commands\" \\",
289            name = p.meta.bin_name.as_ref().unwrap().replace(" ", "__")
290        )
291    } else {
292        String::new()
293    };
294    let sc = if p.has_subcommands() {
295        format!("\"*::: :->{name}\" \\", name = p.meta.name)
296    } else {
297        String::new()
298    };
299
300    if !opts.is_empty() {
301        ret.push(opts);
302    }
303    if !flags.is_empty() {
304        ret.push(flags);
305    }
306    if !positionals.is_empty() {
307        ret.push(positionals);
308    }
309    if !sc_or_a.is_empty() {
310        ret.push(sc_or_a);
311    }
312    if !sc.is_empty() {
313        ret.push(sc);
314    }
315    ret.push(String::from("&& ret=0"));
316
317    ret.join("\n")
318}
319
320// Escape help string inside single quotes and brackets
321fn escape_help(string: &str) -> String {
322    string
323        .replace("\\", "\\\\")
324        .replace("'", "'\\''")
325        .replace("[", "\\[")
326        .replace("]", "\\]")
327}
328
329// Escape value string inside single quotes and parentheses
330fn escape_value(string: &str) -> String {
331    string
332        .replace("\\", "\\\\")
333        .replace("'", "'\\''")
334        .replace("(", "\\(")
335        .replace(")", "\\)")
336        .replace(" ", "\\ ")
337}
338
339fn write_opts_of(p: &Parser) -> String {
340    debugln!("write_opts_of;");
341    let mut ret = vec![];
342    for o in p.opts() {
343        debugln!("write_opts_of:iter: o={}", o.name());
344        let help = o.help().map_or(String::new(), escape_help);
345        let mut conflicts = get_zsh_arg_conflicts!(p, o, INTERNAL_ERROR_MSG);
346        conflicts = if conflicts.is_empty() {
347            String::new()
348        } else {
349            format!("({})", conflicts)
350        };
351
352        let multiple = if o.is_set(ArgSettings::Multiple) {
353            "*"
354        } else {
355            ""
356        };
357        let pv = if let Some(pv_vec) = o.possible_vals() {
358            format!(
359                ": :({})",
360                pv_vec
361                    .iter()
362                    .map(|v| escape_value(*v))
363                    .collect::<Vec<String>>()
364                    .join(" ")
365            )
366        } else {
367            String::new()
368        };
369        if let Some(short) = o.short() {
370            let s = format!(
371                "'{conflicts}{multiple}-{arg}+[{help}]{possible_values}' \\",
372                conflicts = conflicts,
373                multiple = multiple,
374                arg = short,
375                possible_values = pv,
376                help = help
377            );
378
379            debugln!("write_opts_of:iter: Wrote...{}", &*s);
380            ret.push(s);
381        }
382        if let Some(long) = o.long() {
383            let l = format!(
384                "'{conflicts}{multiple}--{arg}=[{help}]{possible_values}' \\",
385                conflicts = conflicts,
386                multiple = multiple,
387                arg = long,
388                possible_values = pv,
389                help = help
390            );
391
392            debugln!("write_opts_of:iter: Wrote...{}", &*l);
393            ret.push(l);
394        }
395    }
396
397    ret.join("\n")
398}
399
400fn write_flags_of(p: &Parser) -> String {
401    debugln!("write_flags_of;");
402    let mut ret = vec![];
403    for f in p.flags() {
404        debugln!("write_flags_of:iter: f={}", f.name());
405        let help = f.help().map_or(String::new(), escape_help);
406        let mut conflicts = get_zsh_arg_conflicts!(p, f, INTERNAL_ERROR_MSG);
407        conflicts = if conflicts.is_empty() {
408            String::new()
409        } else {
410            format!("({})", conflicts)
411        };
412
413        let multiple = if f.is_set(ArgSettings::Multiple) {
414            "*"
415        } else {
416            ""
417        };
418        if let Some(short) = f.short() {
419            let s = format!(
420                "'{conflicts}{multiple}-{arg}[{help}]' \\",
421                multiple = multiple,
422                conflicts = conflicts,
423                arg = short,
424                help = help
425            );
426
427            debugln!("write_flags_of:iter: Wrote...{}", &*s);
428            ret.push(s);
429        }
430
431        if let Some(long) = f.long() {
432            let l = format!(
433                "'{conflicts}{multiple}--{arg}[{help}]' \\",
434                conflicts = conflicts,
435                multiple = multiple,
436                arg = long,
437                help = help
438            );
439
440            debugln!("write_flags_of:iter: Wrote...{}", &*l);
441            ret.push(l);
442        }
443    }
444
445    ret.join("\n")
446}
447
448fn write_positionals_of(p: &Parser) -> String {
449    debugln!("write_positionals_of;");
450    let mut ret = vec![];
451    for arg in p.positionals() {
452        debugln!("write_positionals_of:iter: arg={}", arg.b.name);
453        let a = format!(
454            "'{optional}:{name}{help}:{action}' \\",
455            optional = if !arg.b.is_set(ArgSettings::Required) {
456                ":"
457            } else {
458                ""
459            },
460            name = arg.b.name,
461            help = arg
462                .b
463                .help
464                .map_or("".to_owned(), |v| " -- ".to_owned() + v)
465                .replace("[", "\\[")
466                .replace("]", "\\]"),
467            action = arg.possible_vals().map_or("_files".to_owned(), |values| {
468                format!(
469                    "({})",
470                    values
471                        .iter()
472                        .map(|v| escape_value(*v))
473                        .collect::<Vec<String>>()
474                        .join(" ")
475                )
476            })
477        );
478
479        debugln!("write_positionals_of:iter: Wrote...{}", a);
480        ret.push(a);
481    }
482
483    ret.join("\n")
484}