1use std::io::Write;
2
3use clap::*;
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_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'
149fn 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
217fn 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    ;;
271esac",
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.
283fn 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
319fn 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
358fn 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
423fn 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
432fn escape_value(string: &str) -> String {
433    string
434        .replace('\\', "\\\\")
435        .replace('\'', "'\\''")
436        .replace('(', "\\(")
437        .replace(')', "\\)")
438        .replace(' ', "\\ ")
439}
440
441fn 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
503fn 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
541fn 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
616fn 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