clap_complete/aot/shells/
bash.rs

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