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#[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("") } 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}