1 use std::{fmt::Write as _, io::Write};
2
3 use clap::{Arg, Command, ValueHint};
4
5 use crate::generator::{utils, Generator};
6
7 /// Generate bash completion file
8 #[derive(Copy, Clone, PartialEq, Eq, Debug)]
9 pub struct Bash;
10
11 impl Generator for Bash {
file_name(&self, name: &str) -> String12 fn file_name(&self, name: &str) -> String {
13 format!("{name}.bash")
14 }
15
generate(&self, cmd: &Command, buf: &mut dyn Write)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
63 if [[ \"${{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}
65 else
66 complete -F _{name} -o bashdefault -o default {name}
67 fi
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
all_subcommands(cmd: &Command, parent_fn_name: &str) -> String81 fn 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
subcommand_details(cmd: &Command) -> String128 fn 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
option_details_for_path(cmd: &Command, path: &str) -> String165 fn 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
vals_for(o: &Arg) -> String249 fn 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
all_options_for_path(cmd: &Command, path: &str) -> String270 fn 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 }
298