1 use std::{fmt::Write as _, io::Write};
2
3 use clap::*;
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![
182 format!("--{})", long),
183 format!("COMPREPLY=({})", vals_for(o)),
184 ];
185
186 if let Some(copt) = compopt {
187 v.extend([
188 r#"if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then"#.to_string(),
189 format!(" {}", copt),
190 "fi".to_string(),
191 ]);
192 }
193
194 v.extend(["return 0", ";;"].iter().map(|s| s.to_string()));
195 v.join("\n ")
196 }));
197 }
198
199 if let Some(shorts) = o.get_short_and_visible_aliases() {
200 opts.extend(shorts.iter().map(|short| {
201 let mut v = vec![
202 format!("-{})", short),
203 format!("COMPREPLY=({})", vals_for(o)),
204 ];
205
206 if let Some(copt) = compopt {
207 v.extend([
208 r#"if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then"#.to_string(),
209 format!(" {}", copt),
210 "fi".to_string(),
211 ]);
212 }
213
214 v.extend(["return 0", ";;"].iter().map(|s| s.to_string()));
215 v.join("\n ")
216 }));
217 }
218 }
219
220 opts.join("\n ")
221 }
222
vals_for(o: &Arg) -> String223 fn vals_for(o: &Arg) -> String {
224 debug!("vals_for: o={}", o.get_id());
225
226 if let Some(vals) = crate::generator::utils::possible_values(o) {
227 format!(
228 "$(compgen -W \"{}\" -- \"${{cur}}\")",
229 vals.iter()
230 .filter(|pv| !pv.is_hide_set())
231 .map(|n| n.get_name())
232 .collect::<Vec<_>>()
233 .join(" ")
234 )
235 } else if o.get_value_hint() == ValueHint::DirPath {
236 String::from("") // should be empty to avoid duplicate candidates
237 } else if o.get_value_hint() == ValueHint::Other {
238 String::from("\"${cur}\"")
239 } else {
240 String::from("$(compgen -f \"${cur}\")")
241 }
242 }
243
all_options_for_path(cmd: &Command, path: &str) -> String244 fn all_options_for_path(cmd: &Command, path: &str) -> String {
245 debug!("all_options_for_path: path={path}");
246
247 let p = utils::find_subcommand_with_path(cmd, path.split("__").skip(1).collect());
248
249 let mut opts = String::new();
250 for short in utils::shorts_and_visible_aliases(p) {
251 write!(&mut opts, "-{short} ").unwrap();
252 }
253 for long in utils::longs_and_visible_aliases(p) {
254 write!(&mut opts, "--{long} ").unwrap();
255 }
256 for pos in p.get_positionals() {
257 if let Some(vals) = utils::possible_values(pos) {
258 for value in vals {
259 write!(&mut opts, "{} ", value.get_name()).unwrap();
260 }
261 } else {
262 write!(&mut opts, "{pos} ").unwrap();
263 }
264 }
265 for (sc, _) in utils::subcommands(p) {
266 write!(&mut opts, "{sc} ").unwrap();
267 }
268 opts.pop();
269
270 opts
271 }
272