1 //! Complete commands within shells 2 3 /// Complete commands within bash 4 pub mod bash { 5 use std::ffi::OsString; 6 use std::io::Write; 7 8 use unicode_xid::UnicodeXID; 9 10 #[derive(clap::Subcommand)] 11 #[command(hide = true)] 12 #[allow(missing_docs)] 13 #[derive(Clone, Debug)] 14 pub enum CompleteCommand { 15 /// Register shell completions for this program 16 Complete(CompleteArgs), 17 } 18 19 #[derive(clap::Args)] 20 #[command(group = clap::ArgGroup::new("complete").multiple(true).conflicts_with("register"))] 21 #[allow(missing_docs)] 22 #[derive(Clone, Debug)] 23 pub struct CompleteArgs { 24 /// Path to write completion-registration to 25 #[arg(long, required = true)] 26 register: Option<std::path::PathBuf>, 27 28 #[arg( 29 long, 30 required = true, 31 value_name = "COMP_CWORD", 32 hide_short_help = true, 33 group = "complete" 34 )] 35 index: Option<usize>, 36 37 #[arg(long, hide_short_help = true, group = "complete")] 38 ifs: Option<String>, 39 40 #[arg( 41 long = "type", 42 required = true, 43 hide_short_help = true, 44 group = "complete" 45 )] 46 comp_type: Option<CompType>, 47 48 #[arg(long, hide_short_help = true, group = "complete")] 49 space: bool, 50 51 #[arg( 52 long, 53 conflicts_with = "space", 54 hide_short_help = true, 55 group = "complete" 56 )] 57 no_space: bool, 58 59 #[arg(raw = true, hide_short_help = true, group = "complete")] 60 comp_words: Vec<OsString>, 61 } 62 63 impl CompleteCommand { 64 /// Process the completion request complete(&self, cmd: &mut clap::Command) -> std::convert::Infallible65 pub fn complete(&self, cmd: &mut clap::Command) -> std::convert::Infallible { 66 self.try_complete(cmd).unwrap_or_else(|e| e.exit()); 67 std::process::exit(0) 68 } 69 70 /// Process the completion request try_complete(&self, cmd: &mut clap::Command) -> clap::error::Result<()>71 pub fn try_complete(&self, cmd: &mut clap::Command) -> clap::error::Result<()> { 72 debug!("CompleteCommand::try_complete: {:?}", self); 73 let CompleteCommand::Complete(args) = self; 74 if let Some(out_path) = args.register.as_deref() { 75 let mut buf = Vec::new(); 76 let name = cmd.get_name(); 77 let bin = cmd.get_bin_name().unwrap_or_else(|| cmd.get_name()); 78 register(name, [bin], bin, &Behavior::default(), &mut buf)?; 79 if out_path == std::path::Path::new("-") { 80 std::io::stdout().write_all(&buf)?; 81 } else if out_path.is_dir() { 82 let out_path = out_path.join(file_name(name)); 83 std::fs::write(out_path, buf)?; 84 } else { 85 std::fs::write(out_path, buf)?; 86 } 87 } else { 88 let index = args.index.unwrap_or_default(); 89 let comp_type = args.comp_type.unwrap_or_default(); 90 let space = match (args.space, args.no_space) { 91 (true, false) => Some(true), 92 (false, true) => Some(false), 93 (true, true) => { 94 unreachable!("`--space` and `--no-space` set, clap should prevent this") 95 } 96 (false, false) => None, 97 } 98 .unwrap(); 99 let current_dir = std::env::current_dir().ok(); 100 let completions = complete( 101 cmd, 102 args.comp_words.clone(), 103 index, 104 comp_type, 105 space, 106 current_dir.as_deref(), 107 )?; 108 109 let mut buf = Vec::new(); 110 for (i, completion) in completions.iter().enumerate() { 111 if i != 0 { 112 write!(&mut buf, "{}", args.ifs.as_deref().unwrap_or("\n"))?; 113 } 114 write!(&mut buf, "{}", completion.to_string_lossy())?; 115 } 116 std::io::stdout().write_all(&buf)?; 117 } 118 119 Ok(()) 120 } 121 } 122 123 /// The recommended file name for the registration code file_name(name: &str) -> String124 pub fn file_name(name: &str) -> String { 125 format!("{}.bash", name) 126 } 127 128 /// Define the completion behavior 129 pub enum Behavior { 130 /// Bare bones behavior 131 Minimal, 132 /// Fallback to readline behavior when no matches are generated 133 Readline, 134 /// Customize bash's completion behavior 135 Custom(String), 136 } 137 138 impl Default for Behavior { default() -> Self139 fn default() -> Self { 140 Self::Readline 141 } 142 } 143 144 /// Generate code to register the dynamic completion register( name: &str, executables: impl IntoIterator<Item = impl AsRef<str>>, completer: &str, behavior: &Behavior, buf: &mut dyn Write, ) -> Result<(), std::io::Error>145 pub fn register( 146 name: &str, 147 executables: impl IntoIterator<Item = impl AsRef<str>>, 148 completer: &str, 149 behavior: &Behavior, 150 buf: &mut dyn Write, 151 ) -> Result<(), std::io::Error> { 152 let escaped_name = name.replace('-', "_"); 153 debug_assert!( 154 escaped_name.chars().all(|c| c.is_xid_continue()), 155 "`name` must be an identifier, got `{}`", 156 escaped_name 157 ); 158 let mut upper_name = escaped_name.clone(); 159 upper_name.make_ascii_uppercase(); 160 161 let executables = executables 162 .into_iter() 163 .map(|s| shlex::quote(s.as_ref()).into_owned()) 164 .collect::<Vec<_>>() 165 .join(" "); 166 167 let options = match behavior { 168 Behavior::Minimal => "-o nospace -o bashdefault", 169 Behavior::Readline => "-o nospace -o default -o bashdefault", 170 Behavior::Custom(c) => c.as_str(), 171 }; 172 173 let completer = shlex::quote(completer); 174 175 let script = r#" 176 _clap_complete_NAME() { 177 local IFS=$'\013' 178 local SUPPRESS_SPACE=0 179 if compopt +o nospace 2> /dev/null; then 180 SUPPRESS_SPACE=1 181 fi 182 if [[ ${SUPPRESS_SPACE} == 1 ]]; then 183 SPACE_ARG="--no-space" 184 else 185 SPACE_ARG="--space" 186 fi 187 COMPREPLY=( $("COMPLETER" complete --index ${COMP_CWORD} --type ${COMP_TYPE} ${SPACE_ARG} --ifs="$IFS" -- "${COMP_WORDS[@]}") ) 188 if [[ $? != 0 ]]; then 189 unset COMPREPLY 190 elif [[ $SUPPRESS_SPACE == 1 ]] && [[ "${COMPREPLY-}" =~ [=/:]$ ]]; then 191 compopt -o nospace 192 fi 193 } 194 complete OPTIONS -F _clap_complete_NAME EXECUTABLES 195 "# 196 .replace("NAME", &escaped_name) 197 .replace("EXECUTABLES", &executables) 198 .replace("OPTIONS", options) 199 .replace("COMPLETER", &completer) 200 .replace("UPPER", &upper_name); 201 202 writeln!(buf, "{}", script)?; 203 Ok(()) 204 } 205 206 /// Type of completion attempted that caused a completion function to be called 207 #[derive(Copy, Clone, Debug, PartialEq, Eq)] 208 #[non_exhaustive] 209 pub enum CompType { 210 /// Normal completion 211 Normal, 212 /// List completions after successive tabs 213 Successive, 214 /// List alternatives on partial word completion 215 Alternatives, 216 /// List completions if the word is not unmodified 217 Unmodified, 218 /// Menu completion 219 Menu, 220 } 221 222 impl clap::ValueEnum for CompType { value_variants<'a>() -> &'a [Self]223 fn value_variants<'a>() -> &'a [Self] { 224 &[ 225 Self::Normal, 226 Self::Successive, 227 Self::Alternatives, 228 Self::Unmodified, 229 Self::Menu, 230 ] 231 } to_possible_value(&self) -> ::std::option::Option<clap::builder::PossibleValue>232 fn to_possible_value(&self) -> ::std::option::Option<clap::builder::PossibleValue> { 233 match self { 234 Self::Normal => { 235 let value = "9"; 236 debug_assert_eq!(b'\t'.to_string(), value); 237 Some( 238 clap::builder::PossibleValue::new(value) 239 .alias("normal") 240 .help("Normal completion"), 241 ) 242 } 243 Self::Successive => { 244 let value = "63"; 245 debug_assert_eq!(b'?'.to_string(), value); 246 Some( 247 clap::builder::PossibleValue::new(value) 248 .alias("successive") 249 .help("List completions after successive tabs"), 250 ) 251 } 252 Self::Alternatives => { 253 let value = "33"; 254 debug_assert_eq!(b'!'.to_string(), value); 255 Some( 256 clap::builder::PossibleValue::new(value) 257 .alias("alternatives") 258 .help("List alternatives on partial word completion"), 259 ) 260 } 261 Self::Unmodified => { 262 let value = "64"; 263 debug_assert_eq!(b'@'.to_string(), value); 264 Some( 265 clap::builder::PossibleValue::new(value) 266 .alias("unmodified") 267 .help("List completions if the word is not unmodified"), 268 ) 269 } 270 Self::Menu => { 271 let value = "37"; 272 debug_assert_eq!(b'%'.to_string(), value); 273 Some( 274 clap::builder::PossibleValue::new(value) 275 .alias("menu") 276 .help("Menu completion"), 277 ) 278 } 279 } 280 } 281 } 282 283 impl Default for CompType { default() -> Self284 fn default() -> Self { 285 Self::Normal 286 } 287 } 288 289 /// Complete the command specified complete( cmd: &mut clap::Command, args: Vec<std::ffi::OsString>, arg_index: usize, _comp_type: CompType, _trailing_space: bool, current_dir: Option<&std::path::Path>, ) -> Result<Vec<std::ffi::OsString>, std::io::Error>290 pub fn complete( 291 cmd: &mut clap::Command, 292 args: Vec<std::ffi::OsString>, 293 arg_index: usize, 294 _comp_type: CompType, 295 _trailing_space: bool, 296 current_dir: Option<&std::path::Path>, 297 ) -> Result<Vec<std::ffi::OsString>, std::io::Error> { 298 cmd.build(); 299 300 let raw_args = clap_lex::RawArgs::new(args.into_iter()); 301 let mut cursor = raw_args.cursor(); 302 let mut target_cursor = raw_args.cursor(); 303 raw_args.seek( 304 &mut target_cursor, 305 clap_lex::SeekFrom::Start(arg_index as u64), 306 ); 307 // As we loop, `cursor` will always be pointing to the next item 308 raw_args.next_os(&mut target_cursor); 309 310 // TODO: Multicall support 311 if !cmd.is_no_binary_name_set() { 312 raw_args.next_os(&mut cursor); 313 } 314 315 let mut current_cmd = &*cmd; 316 let mut pos_index = 1; 317 let mut is_escaped = false; 318 while let Some(arg) = raw_args.next(&mut cursor) { 319 if cursor == target_cursor { 320 return complete_arg(&arg, current_cmd, current_dir, pos_index, is_escaped); 321 } 322 323 debug!( 324 "complete::next: Begin parsing '{:?}' ({:?})", 325 arg.to_value_os(), 326 arg.to_value_os().as_raw_bytes() 327 ); 328 329 if let Ok(value) = arg.to_value() { 330 if let Some(next_cmd) = current_cmd.find_subcommand(value) { 331 current_cmd = next_cmd; 332 pos_index = 0; 333 continue; 334 } 335 } 336 337 if is_escaped { 338 pos_index += 1; 339 } else if arg.is_escape() { 340 is_escaped = true; 341 } else if let Some(_long) = arg.to_long() { 342 } else if let Some(_short) = arg.to_short() { 343 } else { 344 pos_index += 1; 345 } 346 } 347 348 Err(std::io::Error::new( 349 std::io::ErrorKind::Other, 350 "No completion generated", 351 )) 352 } 353 complete_arg( arg: &clap_lex::ParsedArg<'_>, cmd: &clap::Command, current_dir: Option<&std::path::Path>, pos_index: usize, is_escaped: bool, ) -> Result<Vec<std::ffi::OsString>, std::io::Error>354 fn complete_arg( 355 arg: &clap_lex::ParsedArg<'_>, 356 cmd: &clap::Command, 357 current_dir: Option<&std::path::Path>, 358 pos_index: usize, 359 is_escaped: bool, 360 ) -> Result<Vec<std::ffi::OsString>, std::io::Error> { 361 debug!( 362 "complete_arg: arg={:?}, cmd={:?}, current_dir={:?}, pos_index={}, is_escaped={}", 363 arg, 364 cmd.get_name(), 365 current_dir, 366 pos_index, 367 is_escaped 368 ); 369 let mut completions = Vec::new(); 370 371 if !is_escaped { 372 if let Some((flag, value)) = arg.to_long() { 373 if let Ok(flag) = flag { 374 if let Some(value) = value { 375 if let Some(arg) = cmd.get_arguments().find(|a| a.get_long() == Some(flag)) 376 { 377 completions.extend( 378 complete_arg_value(value.to_str().ok_or(value), arg, current_dir) 379 .into_iter() 380 .map(|os| { 381 // HACK: Need better `OsStr` manipulation 382 format!("--{}={}", flag, os.to_string_lossy()).into() 383 }), 384 ) 385 } 386 } else { 387 completions.extend( 388 crate::generator::utils::longs_and_visible_aliases(cmd) 389 .into_iter() 390 .filter_map(|f| { 391 f.starts_with(flag).then(|| format!("--{}", f).into()) 392 }), 393 ); 394 } 395 } 396 } else if arg.is_escape() || arg.is_stdio() || arg.is_empty() { 397 // HACK: Assuming knowledge of is_escape / is_stdio 398 completions.extend( 399 crate::generator::utils::longs_and_visible_aliases(cmd) 400 .into_iter() 401 .map(|f| format!("--{}", f).into()), 402 ); 403 } 404 405 if arg.is_empty() || arg.is_stdio() || arg.is_short() { 406 // HACK: Assuming knowledge of is_stdio 407 completions.extend( 408 crate::generator::utils::shorts_and_visible_aliases(cmd) 409 .into_iter() 410 // HACK: Need better `OsStr` manipulation 411 .map(|f| format!("{}{}", arg.to_value_os().to_str_lossy(), f).into()), 412 ); 413 } 414 } 415 416 if let Some(positional) = cmd 417 .get_positionals() 418 .find(|p| p.get_index() == Some(pos_index)) 419 { 420 completions.extend(complete_arg_value(arg.to_value(), positional, current_dir)); 421 } 422 423 if let Ok(value) = arg.to_value() { 424 completions.extend(complete_subcommand(value, cmd)); 425 } 426 427 Ok(completions) 428 } 429 complete_arg_value( value: Result<&str, &clap_lex::RawOsStr>, arg: &clap::Arg, current_dir: Option<&std::path::Path>, ) -> Vec<OsString>430 fn complete_arg_value( 431 value: Result<&str, &clap_lex::RawOsStr>, 432 arg: &clap::Arg, 433 current_dir: Option<&std::path::Path>, 434 ) -> Vec<OsString> { 435 let mut values = Vec::new(); 436 debug!("complete_arg_value: arg={:?}, value={:?}", arg, value); 437 438 if let Some(possible_values) = crate::generator::utils::possible_values(arg) { 439 if let Ok(value) = value { 440 values.extend(possible_values.into_iter().filter_map(|p| { 441 let name = p.get_name(); 442 name.starts_with(value).then(|| name.into()) 443 })); 444 } 445 } else { 446 let value_os = match value { 447 Ok(value) => clap_lex::RawOsStr::from_str(value), 448 Err(value_os) => value_os, 449 }; 450 match arg.get_value_hint() { 451 clap::ValueHint::Other => { 452 // Should not complete 453 } 454 clap::ValueHint::Unknown | clap::ValueHint::AnyPath => { 455 values.extend(complete_path(value_os, current_dir, |_| true)); 456 } 457 clap::ValueHint::FilePath => { 458 values.extend(complete_path(value_os, current_dir, |p| p.is_file())); 459 } 460 clap::ValueHint::DirPath => { 461 values.extend(complete_path(value_os, current_dir, |p| p.is_dir())); 462 } 463 clap::ValueHint::ExecutablePath => { 464 use is_executable::IsExecutable; 465 values.extend(complete_path(value_os, current_dir, |p| p.is_executable())); 466 } 467 clap::ValueHint::CommandName 468 | clap::ValueHint::CommandString 469 | clap::ValueHint::CommandWithArguments 470 | clap::ValueHint::Username 471 | clap::ValueHint::Hostname 472 | clap::ValueHint::Url 473 | clap::ValueHint::EmailAddress => { 474 // No completion implementation 475 } 476 _ => { 477 // Safe-ish fallback 478 values.extend(complete_path(value_os, current_dir, |_| true)); 479 } 480 } 481 values.sort(); 482 } 483 484 values 485 } 486 complete_path( value_os: &clap_lex::RawOsStr, current_dir: Option<&std::path::Path>, is_wanted: impl Fn(&std::path::Path) -> bool, ) -> Vec<OsString>487 fn complete_path( 488 value_os: &clap_lex::RawOsStr, 489 current_dir: Option<&std::path::Path>, 490 is_wanted: impl Fn(&std::path::Path) -> bool, 491 ) -> Vec<OsString> { 492 let mut completions = Vec::new(); 493 494 let current_dir = match current_dir { 495 Some(current_dir) => current_dir, 496 None => { 497 // Can't complete without a `current_dir` 498 return Vec::new(); 499 } 500 }; 501 let (existing, prefix) = value_os 502 .split_once('\\') 503 .unwrap_or((clap_lex::RawOsStr::from_str(""), value_os)); 504 let root = current_dir.join(existing.to_os_str()); 505 debug!("complete_path: root={:?}, prefix={:?}", root, prefix); 506 507 for entry in std::fs::read_dir(&root) 508 .ok() 509 .into_iter() 510 .flatten() 511 .filter_map(Result::ok) 512 { 513 let raw_file_name = clap_lex::RawOsString::new(entry.file_name()); 514 if !raw_file_name.starts_with_os(prefix) { 515 continue; 516 } 517 518 if entry.metadata().map(|m| m.is_dir()).unwrap_or(false) { 519 let path = entry.path(); 520 let mut suggestion = pathdiff::diff_paths(&path, current_dir).unwrap_or(path); 521 suggestion.push(""); // Ensure trailing `/` 522 completions.push(suggestion.as_os_str().to_owned()); 523 } else { 524 let path = entry.path(); 525 if is_wanted(&path) { 526 let suggestion = pathdiff::diff_paths(&path, current_dir).unwrap_or(path); 527 completions.push(suggestion.as_os_str().to_owned()); 528 } 529 } 530 } 531 532 completions 533 } 534 complete_subcommand(value: &str, cmd: &clap::Command) -> Vec<OsString>535 fn complete_subcommand(value: &str, cmd: &clap::Command) -> Vec<OsString> { 536 debug!( 537 "complete_subcommand: cmd={:?}, value={:?}", 538 cmd.get_name(), 539 value 540 ); 541 542 let mut scs = crate::generator::utils::all_subcommands(cmd) 543 .into_iter() 544 .filter(|x| x.0.starts_with(value)) 545 .map(|x| OsString::from(&x.0)) 546 .collect::<Vec<_>>(); 547 scs.sort(); 548 scs.dedup(); 549 scs 550 } 551 } 552