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