1use std::{fmt::Write as _, io::Write};
2
3use clap::*;
4
5use crate::generator::{utils, Generator};
6
7/// Generate bash completion file
8#[derive(Copy, Clone, PartialEq, Eq, Debug)]
9pub struct Bash;
10
11impl Generator for Bash {
12    fn file_name(&self, name: &str) -> String {
13        format!("{name}.bash")
14    }
15
16    fn generate(&self, cmd: &Command, buf: &mut dyn Write) {
17        let bin_name = cmd
18            .get_bin_name()
19            .expect("crate::generate should have set the bin_name");
20
21        w!(
22            buf,
23            format!(
24                "_{name}() {{
25    local i cur prev opts cmd
26    COMPREPLY=()
27    cur=\"${{COMP_WORDS[COMP_CWORD]}}\"
28    prev=\"${{COMP_WORDS[COMP_CWORD-1]}}\"
29    cmd=\"\"
30    opts=\"\"
31
32    for i in ${{COMP_WORDS[@]}}
33    do
34        case \"${{cmd}},${{i}}\" in
35            \",$1\")
36                cmd=\"{cmd}\"
37                ;;{subcmds}
38            *)
39                ;;
40        esac
41    done
42
43    case \"${{cmd}}\" in
44        {cmd})
45            opts=\"{name_opts}\"
46            if [[ ${{cur}} == -* || ${{COMP_CWORD}} -eq 1 ]] ; then
47                COMPREPLY=( $(compgen -W \"${{opts}}\" -- \"${{cur}}\") )
48                return 0
49            fi
50            case \"${{prev}}\" in{name_opts_details}
51                *)
52                    COMPREPLY=()
53                    ;;
54            esac
55            COMPREPLY=( $(compgen -W \"${{opts}}\" -- \"${{cur}}\") )
56            return 0
57            ;;{subcmd_details}
58    esac
59}}
60
61complete -F _{name} -o bashdefault -o default {name}
62",
63                name = bin_name,
64                cmd = bin_name.replace('-', "__"),
65                name_opts = all_options_for_path(cmd, bin_name),
66                name_opts_details = option_details_for_path(cmd, bin_name),
67                subcmds = all_subcommands(cmd),
68                subcmd_details = subcommand_details(cmd)
69            )
70            .as_bytes()
71        );
72    }
73}
74
75fn all_subcommands(cmd: &Command) -> String {
76    debug!("all_subcommands");
77
78    fn add_command(
79        parent_fn_name: &str,
80        cmd: &Command,
81        subcmds: &mut Vec<(String, String, String)>,
82    ) {
83        let fn_name = format!(
84            "{parent_fn_name}__{cmd_name}",
85            parent_fn_name = parent_fn_name,
86            cmd_name = cmd.get_name().to_string().replace('-', "__")
87        );
88        subcmds.push((
89            parent_fn_name.to_string(),
90            cmd.get_name().to_string(),
91            fn_name.clone(),
92        ));
93        for alias in cmd.get_visible_aliases() {
94            subcmds.push((
95                parent_fn_name.to_string(),
96                alias.to_string(),
97                fn_name.clone(),
98            ));
99        }
100        for subcmd in cmd.get_subcommands() {
101            add_command(&fn_name, subcmd, subcmds);
102        }
103    }
104    let mut subcmds = vec![];
105    let fn_name = cmd.get_name().replace('-', "__");
106    for subcmd in cmd.get_subcommands() {
107        add_command(&fn_name, subcmd, &mut subcmds);
108    }
109    subcmds.sort();
110
111    let mut cases = vec![String::new()];
112    for (parent_fn_name, name, fn_name) in subcmds {
113        cases.push(format!(
114            "{parent_fn_name},{name})
115                cmd=\"{fn_name}\"
116                ;;",
117        ));
118    }
119
120    cases.join("\n            ")
121}
122
123fn subcommand_details(cmd: &Command) -> String {
124    debug!("subcommand_details");
125
126    let mut subcmd_dets = vec![String::new()];
127    let mut scs = utils::all_subcommands(cmd)
128        .iter()
129        .map(|x| x.1.replace(' ', "__"))
130        .collect::<Vec<_>>();
131
132    scs.sort();
133
134    subcmd_dets.extend(scs.iter().map(|sc| {
135        format!(
136            "{subcmd})
137            opts=\"{sc_opts}\"
138            if [[ ${{cur}} == -* || ${{COMP_CWORD}} -eq {level} ]] ; then
139                COMPREPLY=( $(compgen -W \"${{opts}}\" -- \"${{cur}}\") )
140                return 0
141            fi
142            case \"${{prev}}\" in{opts_details}
143                *)
144                    COMPREPLY=()
145                    ;;
146            esac
147            COMPREPLY=( $(compgen -W \"${{opts}}\" -- \"${{cur}}\") )
148            return 0
149            ;;",
150            subcmd = sc.replace('-', "__"),
151            sc_opts = all_options_for_path(cmd, sc),
152            level = sc.split("__").map(|_| 1).sum::<u64>(),
153            opts_details = option_details_for_path(cmd, sc)
154        )
155    }));
156
157    subcmd_dets.join("\n        ")
158}
159
160fn option_details_for_path(cmd: &Command, path: &str) -> String {
161    debug!("option_details_for_path: path={}", path);
162
163    let p = utils::find_subcommand_with_path(cmd, path.split("__").skip(1).collect());
164    let mut opts = vec![String::new()];
165
166    for o in p.get_opts() {
167        if let Some(longs) = o.get_long_and_visible_aliases() {
168            opts.extend(longs.iter().map(|long| {
169                format!(
170                    "--{})
171                    COMPREPLY=({})
172                    return 0
173                    ;;",
174                    long,
175                    vals_for(o)
176                )
177            }));
178        }
179
180        if let Some(shorts) = o.get_short_and_visible_aliases() {
181            opts.extend(shorts.iter().map(|short| {
182                format!(
183                    "-{})
184                    COMPREPLY=({})
185                    return 0
186                    ;;",
187                    short,
188                    vals_for(o)
189                )
190            }));
191        }
192    }
193
194    opts.join("\n                ")
195}
196
197fn vals_for(o: &Arg) -> String {
198    debug!("vals_for: o={}", o.get_id());
199
200    if let Some(vals) = crate::generator::utils::possible_values(o) {
201        format!(
202            "$(compgen -W \"{}\" -- \"${{cur}}\")",
203            vals.iter()
204                .filter(|pv| !pv.is_hide_set())
205                .map(|n| n.get_name())
206                .collect::<Vec<_>>()
207                .join(" ")
208        )
209    } else {
210        String::from("$(compgen -f \"${cur}\")")
211    }
212}
213
214fn all_options_for_path(cmd: &Command, path: &str) -> String {
215    debug!("all_options_for_path: path={}", path);
216
217    let p = utils::find_subcommand_with_path(cmd, path.split("__").skip(1).collect());
218
219    let mut opts = String::new();
220    for short in utils::shorts_and_visible_aliases(p) {
221        write!(&mut opts, "-{short} ").unwrap();
222    }
223    for long in utils::longs_and_visible_aliases(p) {
224        write!(&mut opts, "--{long} ").unwrap();
225    }
226    for pos in p.get_positionals() {
227        if let Some(vals) = utils::possible_values(pos) {
228            for value in vals {
229                write!(&mut opts, "{} ", value.get_name()).unwrap();
230            }
231        } else {
232            write!(&mut opts, "{pos} ").unwrap();
233        }
234    }
235    for (sc, _) in utils::subcommands(p) {
236        write!(&mut opts, "{sc} ").unwrap();
237    }
238    opts.pop();
239
240    opts
241}
242