1use std::{fmt::Write as _, io::Write}; 2 3use clap::*; 4 5use crate::generator::{utils, Generator}; 6 7/// Generate bash completion file 8#[derive(Copy, Clone, PartialEq, Eq, Debug)] 9pub struct Bash; 10 11impl Generator for Bash { 12 fn file_name(&self, name: &str) -> String { 13 format!("{name}.bash") 14 } 15 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 w!( 22 buf, 23 format!( 24 "_{name}() {{ 25 local i cur prev opts cmd 26 COMPREPLY=() 27 cur=\"${{COMP_WORDS[COMP_CWORD]}}\" 28 prev=\"${{COMP_WORDS[COMP_CWORD-1]}}\" 29 cmd=\"\" 30 opts=\"\" 31 32 for i in ${{COMP_WORDS[@]}} 33 do 34 case \"${{cmd}},${{i}}\" in 35 \",$1\") 36 cmd=\"{cmd}\" 37 ;;{subcmds} 38 *) 39 ;; 40 esac 41 done 42 43 case \"${{cmd}}\" in 44 {cmd}) 45 opts=\"{name_opts}\" 46 if [[ ${{cur}} == -* || ${{COMP_CWORD}} -eq 1 ]] ; then 47 COMPREPLY=( $(compgen -W \"${{opts}}\" -- \"${{cur}}\") ) 48 return 0 49 fi 50 case \"${{prev}}\" in{name_opts_details} 51 *) 52 COMPREPLY=() 53 ;; 54 esac 55 COMPREPLY=( $(compgen -W \"${{opts}}\" -- \"${{cur}}\") ) 56 return 0 57 ;;{subcmd_details} 58 esac 59}} 60 61complete -F _{name} -o bashdefault -o default {name} 62", 63 name = bin_name, 64 cmd = bin_name.replace('-', "__"), 65 name_opts = all_options_for_path(cmd, bin_name), 66 name_opts_details = option_details_for_path(cmd, bin_name), 67 subcmds = all_subcommands(cmd), 68 subcmd_details = subcommand_details(cmd) 69 ) 70 .as_bytes() 71 ); 72 } 73} 74 75fn all_subcommands(cmd: &Command) -> String { 76 debug!("all_subcommands"); 77 78 fn add_command( 79 parent_fn_name: &str, 80 cmd: &Command, 81 subcmds: &mut Vec<(String, String, String)>, 82 ) { 83 let fn_name = format!( 84 "{parent_fn_name}__{cmd_name}", 85 parent_fn_name = parent_fn_name, 86 cmd_name = cmd.get_name().to_string().replace('-', "__") 87 ); 88 subcmds.push(( 89 parent_fn_name.to_string(), 90 cmd.get_name().to_string(), 91 fn_name.clone(), 92 )); 93 for alias in cmd.get_visible_aliases() { 94 subcmds.push(( 95 parent_fn_name.to_string(), 96 alias.to_string(), 97 fn_name.clone(), 98 )); 99 } 100 for subcmd in cmd.get_subcommands() { 101 add_command(&fn_name, subcmd, subcmds); 102 } 103 } 104 let mut subcmds = vec![]; 105 let fn_name = cmd.get_name().replace('-', "__"); 106 for subcmd in cmd.get_subcommands() { 107 add_command(&fn_name, subcmd, &mut subcmds); 108 } 109 subcmds.sort(); 110 111 let mut cases = vec![String::new()]; 112 for (parent_fn_name, name, fn_name) in subcmds { 113 cases.push(format!( 114 "{parent_fn_name},{name}) 115 cmd=\"{fn_name}\" 116 ;;", 117 )); 118 } 119 120 cases.join("\n ") 121} 122 123fn subcommand_details(cmd: &Command) -> String { 124 debug!("subcommand_details"); 125 126 let mut subcmd_dets = vec![String::new()]; 127 let mut scs = utils::all_subcommands(cmd) 128 .iter() 129 .map(|x| x.1.replace(' ', "__")) 130 .collect::<Vec<_>>(); 131 132 scs.sort(); 133 134 subcmd_dets.extend(scs.iter().map(|sc| { 135 format!( 136 "{subcmd}) 137 opts=\"{sc_opts}\" 138 if [[ ${{cur}} == -* || ${{COMP_CWORD}} -eq {level} ]] ; then 139 COMPREPLY=( $(compgen -W \"${{opts}}\" -- \"${{cur}}\") ) 140 return 0 141 fi 142 case \"${{prev}}\" in{opts_details} 143 *) 144 COMPREPLY=() 145 ;; 146 esac 147 COMPREPLY=( $(compgen -W \"${{opts}}\" -- \"${{cur}}\") ) 148 return 0 149 ;;", 150 subcmd = sc.replace('-', "__"), 151 sc_opts = all_options_for_path(cmd, sc), 152 level = sc.split("__").map(|_| 1).sum::<u64>(), 153 opts_details = option_details_for_path(cmd, sc) 154 ) 155 })); 156 157 subcmd_dets.join("\n ") 158} 159 160fn option_details_for_path(cmd: &Command, path: &str) -> String { 161 debug!("option_details_for_path: path={}", path); 162 163 let p = utils::find_subcommand_with_path(cmd, path.split("__").skip(1).collect()); 164 let mut opts = vec![String::new()]; 165 166 for o in p.get_opts() { 167 if let Some(longs) = o.get_long_and_visible_aliases() { 168 opts.extend(longs.iter().map(|long| { 169 format!( 170 "--{}) 171 COMPREPLY=({}) 172 return 0 173 ;;", 174 long, 175 vals_for(o) 176 ) 177 })); 178 } 179 180 if let Some(shorts) = o.get_short_and_visible_aliases() { 181 opts.extend(shorts.iter().map(|short| { 182 format!( 183 "-{}) 184 COMPREPLY=({}) 185 return 0 186 ;;", 187 short, 188 vals_for(o) 189 ) 190 })); 191 } 192 } 193 194 opts.join("\n ") 195} 196 197fn vals_for(o: &Arg) -> String { 198 debug!("vals_for: o={}", o.get_id()); 199 200 if let Some(vals) = crate::generator::utils::possible_values(o) { 201 format!( 202 "$(compgen -W \"{}\" -- \"${{cur}}\")", 203 vals.iter() 204 .filter(|pv| !pv.is_hide_set()) 205 .map(|n| n.get_name()) 206 .collect::<Vec<_>>() 207 .join(" ") 208 ) 209 } else { 210 String::from("$(compgen -f \"${cur}\")") 211 } 212} 213 214fn all_options_for_path(cmd: &Command, path: &str) -> String { 215 debug!("all_options_for_path: path={}", path); 216 217 let p = utils::find_subcommand_with_path(cmd, path.split("__").skip(1).collect()); 218 219 let mut opts = String::new(); 220 for short in utils::shorts_and_visible_aliases(p) { 221 write!(&mut opts, "-{short} ").unwrap(); 222 } 223 for long in utils::longs_and_visible_aliases(p) { 224 write!(&mut opts, "--{long} ").unwrap(); 225 } 226 for pos in p.get_positionals() { 227 if let Some(vals) = utils::possible_values(pos) { 228 for value in vals { 229 write!(&mut opts, "{} ", value.get_name()).unwrap(); 230 } 231 } else { 232 write!(&mut opts, "{pos} ").unwrap(); 233 } 234 } 235 for (sc, _) in utils::subcommands(p) { 236 write!(&mut opts, "{sc} ").unwrap(); 237 } 238 opts.pop(); 239 240 opts 241} 242