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