clap_complete/aot/shells/
bash.rs

1use std::{fmt::Write as _, io::Write};
2
3use clap::{Arg, Command, ValueHint};
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        let fn_name = bin_name.replace('-', "__");
22
23        write!(
24            buf,
25            "_{name}() {{
26    local i cur prev opts cmd
27    COMPREPLY=()
28    cur=\"${{COMP_WORDS[COMP_CWORD]}}\"
29    prev=\"${{COMP_WORDS[COMP_CWORD-1]}}\"
30    cmd=\"\"
31    opts=\"\"
32
33    for i in ${{COMP_WORDS[@]}}
34    do
35        case \"${{cmd}},${{i}}\" in
36            \",$1\")
37                cmd=\"{cmd}\"
38                ;;{subcmds}
39            *)
40                ;;
41        esac
42    done
43
44    case \"${{cmd}}\" in
45        {cmd})
46            opts=\"{name_opts}\"
47            if [[ ${{cur}} == -* || ${{COMP_CWORD}} -eq 1 ]] ; then
48                COMPREPLY=( $(compgen -W \"${{opts}}\" -- \"${{cur}}\") )
49                return 0
50            fi
51            case \"${{prev}}\" in{name_opts_details}
52                *)
53                    COMPREPLY=()
54                    ;;
55            esac
56            COMPREPLY=( $(compgen -W \"${{opts}}\" -- \"${{cur}}\") )
57            return 0
58            ;;{subcmd_details}
59    esac
60}}
61
62if [[ \"${{BASH_VERSINFO[0]}}\" -eq 4 && \"${{BASH_VERSINFO[1]}}\" -ge 4 || \"${{BASH_VERSINFO[0]}}\" -gt 4 ]]; then
63    complete -F _{name} -o nosort -o bashdefault -o default {name}
64else
65    complete -F _{name} -o bashdefault -o default {name}
66fi
67",
68            name = bin_name,
69            cmd = fn_name,
70            name_opts = all_options_for_path(cmd, bin_name),
71            name_opts_details = option_details_for_path(cmd, bin_name),
72            subcmds = all_subcommands(cmd, &fn_name),
73            subcmd_details = subcommand_details(cmd)
74        ).expect("failed to write completion file");
75    }
76}
77
78fn all_subcommands(cmd: &Command, parent_fn_name: &str) -> String {
79    debug!("all_subcommands");
80
81    fn add_command(
82        parent_fn_name: &str,
83        cmd: &Command,
84        subcmds: &mut Vec<(String, String, String)>,
85    ) {
86        let fn_name = format!(
87            "{parent_fn_name}__{cmd_name}",
88            parent_fn_name = parent_fn_name,
89            cmd_name = cmd.get_name().to_string().replace('-', "__")
90        );
91        subcmds.push((
92            parent_fn_name.to_string(),
93            cmd.get_name().to_string(),
94            fn_name.clone(),
95        ));
96        for alias in cmd.get_visible_aliases() {
97            subcmds.push((
98                parent_fn_name.to_string(),
99                alias.to_string(),
100                fn_name.clone(),
101            ));
102        }
103        for subcmd in cmd.get_subcommands() {
104            add_command(&fn_name, subcmd, subcmds);
105        }
106    }
107    let mut subcmds = vec![];
108    for subcmd in cmd.get_subcommands() {
109        add_command(parent_fn_name, subcmd, &mut subcmds);
110    }
111    subcmds.sort();
112
113    let mut cases = vec![String::new()];
114    for (parent_fn_name, name, fn_name) in subcmds {
115        cases.push(format!(
116            "{parent_fn_name},{name})
117                cmd=\"{fn_name}\"
118                ;;",
119        ));
120    }
121
122    cases.join("\n            ")
123}
124
125fn subcommand_details(cmd: &Command) -> String {
126    debug!("subcommand_details");
127
128    let mut subcmd_dets = vec![String::new()];
129    let mut scs = utils::all_subcommands(cmd)
130        .iter()
131        .map(|x| x.1.replace(' ', "__"))
132        .collect::<Vec<_>>();
133
134    scs.sort();
135    scs.dedup();
136
137    subcmd_dets.extend(scs.iter().map(|sc| {
138        format!(
139            "{subcmd})
140            opts=\"{sc_opts}\"
141            if [[ ${{cur}} == -* || ${{COMP_CWORD}} -eq {level} ]] ; then
142                COMPREPLY=( $(compgen -W \"${{opts}}\" -- \"${{cur}}\") )
143                return 0
144            fi
145            case \"${{prev}}\" in{opts_details}
146                *)
147                    COMPREPLY=()
148                    ;;
149            esac
150            COMPREPLY=( $(compgen -W \"${{opts}}\" -- \"${{cur}}\") )
151            return 0
152            ;;",
153            subcmd = sc.replace('-', "__"),
154            sc_opts = all_options_for_path(cmd, sc),
155            level = sc.split("__").map(|_| 1).sum::<u64>(),
156            opts_details = option_details_for_path(cmd, sc)
157        )
158    }));
159
160    subcmd_dets.join("\n        ")
161}
162
163fn option_details_for_path(cmd: &Command, path: &str) -> String {
164    debug!("option_details_for_path: path={path}");
165
166    let p = utils::find_subcommand_with_path(cmd, path.split("__").skip(1).collect());
167    let mut opts = vec![String::new()];
168
169    for o in p.get_opts() {
170        let compopt = match o.get_value_hint() {
171            ValueHint::FilePath => Some("compopt -o filenames"),
172            ValueHint::DirPath => Some("compopt -o plusdirs"),
173            ValueHint::Other => Some("compopt -o nospace"),
174            _ => None,
175        };
176
177        if let Some(longs) = o.get_long_and_visible_aliases() {
178            opts.extend(longs.iter().map(|long| {
179                let mut v = vec![format!("--{})", long)];
180
181                if o.get_value_hint() == ValueHint::FilePath {
182                    v.extend([
183                        "local oldifs".to_string(),
184                        r#"if [ -n "${IFS+x}" ]; then"#.to_string(),
185                        r#"    oldifs="$IFS""#.to_string(),
186                        "fi".to_string(),
187                        r#"IFS=$'\n'"#.to_string(),
188                        format!("COMPREPLY=({})", vals_for(o)),
189                        r#"if [ -n "${oldifs+x}" ]; then"#.to_string(),
190                        r#"    IFS="$oldifs""#.to_string(),
191                        "fi".to_string(),
192                    ]);
193                } else {
194                    v.push(format!("COMPREPLY=({})", vals_for(o)));
195                }
196
197                if let Some(copt) = compopt {
198                    v.extend([
199                        r#"if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then"#.to_string(),
200                        format!("    {copt}"),
201                        "fi".to_string(),
202                    ]);
203                }
204
205                v.extend(["return 0", ";;"].iter().map(|s| (*s).to_string()));
206                v.join("\n                    ")
207            }));
208        }
209
210        if let Some(shorts) = o.get_short_and_visible_aliases() {
211            opts.extend(shorts.iter().map(|short| {
212                let mut v = vec![format!("-{})", short)];
213
214                if o.get_value_hint() == ValueHint::FilePath {
215                    v.extend([
216                        "local oldifs".to_string(),
217                        r#"if [ -n "${IFS+x}" ]; then"#.to_string(),
218                        r#"    oldifs="$IFS""#.to_string(),
219                        "fi".to_string(),
220                        r#"IFS=$'\n'"#.to_string(),
221                        format!("COMPREPLY=({})", vals_for(o)),
222                        r#"if [ -n "${oldifs+x}" ]; then"#.to_string(),
223                        r#"    IFS="$oldifs""#.to_string(),
224                        "fi".to_string(),
225                    ]);
226                } else {
227                    v.push(format!("COMPREPLY=({})", vals_for(o)));
228                }
229
230                if let Some(copt) = compopt {
231                    v.extend([
232                        r#"if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then"#.to_string(),
233                        format!("    {copt}"),
234                        "fi".to_string(),
235                    ]);
236                }
237
238                v.extend(["return 0", ";;"].iter().map(|s| (*s).to_string()));
239                v.join("\n                    ")
240            }));
241        }
242    }
243
244    opts.join("\n                ")
245}
246
247fn vals_for(o: &Arg) -> String {
248    debug!("vals_for: o={}", o.get_id());
249
250    if let Some(vals) = utils::possible_values(o) {
251        format!(
252            "$(compgen -W \"{}\" -- \"${{cur}}\")",
253            vals.iter()
254                .filter(|pv| !pv.is_hide_set())
255                .map(|n| n.get_name())
256                .collect::<Vec<_>>()
257                .join(" ")
258        )
259    } else if o.get_value_hint() == ValueHint::DirPath {
260        String::from("") // should be empty to avoid duplicate candidates
261    } else if o.get_value_hint() == ValueHint::Other {
262        String::from("\"${cur}\"")
263    } else {
264        String::from("$(compgen -f \"${cur}\")")
265    }
266}
267
268fn all_options_for_path(cmd: &Command, path: &str) -> String {
269    debug!("all_options_for_path: path={path}");
270
271    let p = utils::find_subcommand_with_path(cmd, path.split("__").skip(1).collect());
272
273    let mut opts = String::new();
274    for short in utils::shorts_and_visible_aliases(p) {
275        write!(&mut opts, "-{short} ").expect("writing to String is infallible");
276    }
277    for long in utils::longs_and_visible_aliases(p) {
278        write!(&mut opts, "--{long} ").expect("writing to String is infallible");
279    }
280    for pos in p.get_positionals() {
281        if let Some(vals) = utils::possible_values(pos) {
282            for value in vals {
283                write!(&mut opts, "{} ", value.get_name())
284                    .expect("writing to String is infallible");
285            }
286        } else {
287            write!(&mut opts, "{pos} ").expect("writing to String is infallible");
288        }
289    }
290    for (sc, _) in utils::subcommands(p) {
291        write!(&mut opts, "{sc} ").expect("writing to String is infallible");
292    }
293    opts.pop();
294
295    opts
296}