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