1use std::io::Write; 2 3use clap::*; 4 5use crate::generator::{utils, Generator}; 6use crate::INTERNAL_ERROR_MSG; 7 8/// Generate zsh completion file 9#[derive(Copy, Clone, PartialEq, Eq, Debug)] 10pub struct Zsh; 11 12impl Generator for Zsh { 13 fn file_name(&self, name: &str) -> String { 14 format!("_{name}") 15 } 16 17 fn generate(&self, cmd: &Command, buf: &mut dyn Write) { 18 let bin_name = cmd 19 .get_bin_name() 20 .expect("crate::generate should have set the bin_name"); 21 22 w!( 23 buf, 24 format!( 25 "#compdef {name} 26 27autoload -U is-at-least 28 29_{name}() {{ 30 typeset -A opt_args 31 typeset -a _arguments_options 32 local ret=1 33 34 if is-at-least 5.2; then 35 _arguments_options=(-s -S -C) 36 else 37 _arguments_options=(-s -C) 38 fi 39 40 local context curcontext=\"$curcontext\" state line 41 {initial_args}{subcommands} 42}} 43 44{subcommand_details} 45 46if [ \"$funcstack[1]\" = \"_{name}\" ]; then 47 _{name} \"$@\" 48else 49 compdef _{name} {name} 50fi 51", 52 name = bin_name, 53 initial_args = get_args_of(cmd, None), 54 subcommands = get_subcommands_of(cmd), 55 subcommand_details = subcommand_details(cmd) 56 ) 57 .as_bytes() 58 ); 59 } 60} 61 62// Displays the commands of a subcommand 63// (( $+functions[_[bin_name_underscore]_commands] )) || 64// _[bin_name_underscore]_commands() { 65// local commands; commands=( 66// '[arg_name]:[arg_help]' 67// ) 68// _describe -t commands '[bin_name] commands' commands "$@" 69// 70// Where the following variables are present: 71// [bin_name_underscore]: The full space delineated bin_name, where spaces have been replaced by 72// underscore characters 73// [arg_name]: The name of the subcommand 74// [arg_help]: The help message of the subcommand 75// [bin_name]: The full space delineated bin_name 76// 77// Here's a snippet from rustup: 78// 79// (( $+functions[_rustup_commands] )) || 80// _rustup_commands() { 81// local commands; commands=( 82// 'show:Show the active and installed toolchains' 83// 'update:Update Rust toolchains' 84// # ... snip for brevity 85// 'help:Print this message or the help of the given subcommand(s)' 86// ) 87// _describe -t commands 'rustup commands' commands "$@" 88// 89fn subcommand_details(p: &Command) -> String { 90 debug!("subcommand_details"); 91 92 let bin_name = p 93 .get_bin_name() 94 .expect("crate::generate should have set the bin_name"); 95 96 let mut ret = vec![]; 97 98 // First we do ourself 99 let parent_text = format!( 100 "\ 101(( $+functions[_{bin_name_underscore}_commands] )) || 102_{bin_name_underscore}_commands() {{ 103 local commands; commands=({subcommands_and_args}) 104 _describe -t commands '{bin_name} commands' commands \"$@\" 105}}", 106 bin_name_underscore = bin_name.replace(' ', "__"), 107 bin_name = bin_name, 108 subcommands_and_args = subcommands_of(p) 109 ); 110 ret.push(parent_text); 111 112 // Next we start looping through all the children, grandchildren, etc. 113 let mut all_subcommands = utils::all_subcommands(p); 114 115 all_subcommands.sort(); 116 all_subcommands.dedup(); 117 118 for (_, ref bin_name) in &all_subcommands { 119 debug!("subcommand_details:iter: bin_name={bin_name}"); 120 121 ret.push(format!( 122 "\ 123(( $+functions[_{bin_name_underscore}_commands] )) || 124_{bin_name_underscore}_commands() {{ 125 local commands; commands=({subcommands_and_args}) 126 _describe -t commands '{bin_name} commands' commands \"$@\" 127}}", 128 bin_name_underscore = bin_name.replace(' ', "__"), 129 bin_name = bin_name, 130 subcommands_and_args = 131 subcommands_of(parser_of(p, bin_name).expect(INTERNAL_ERROR_MSG)) 132 )); 133 } 134 135 ret.join("\n") 136} 137 138// Generates subcommand completions in form of 139// 140// '[arg_name]:[arg_help]' 141// 142// Where: 143// [arg_name]: the subcommand's name 144// [arg_help]: the help message of the subcommand 145// 146// A snippet from rustup: 147// 'show:Show the active and installed toolchains' 148// 'update:Update Rust toolchains' 149fn subcommands_of(p: &Command) -> String { 150 debug!("subcommands_of"); 151 152 let mut segments = vec![]; 153 154 fn add_subcommands(subcommand: &Command, name: &str, ret: &mut Vec<String>) { 155 debug!("add_subcommands"); 156 157 let text = format!( 158 "'{name}:{help}' \\", 159 name = name, 160 help = escape_help(&subcommand.get_about().unwrap_or_default().to_string()) 161 ); 162 163 ret.push(text); 164 } 165 166 // The subcommands 167 for command in p.get_subcommands() { 168 debug!("subcommands_of:iter: subcommand={}", command.get_name()); 169 170 add_subcommands(command, command.get_name(), &mut segments); 171 172 for alias in command.get_visible_aliases() { 173 add_subcommands(command, alias, &mut segments); 174 } 175 } 176 177 // Surround the text with newlines for proper formatting. 178 // We need this to prevent weirdly formatted `command=(\n \n)` sections. 179 // When there are no (sub-)commands. 180 if !segments.is_empty() { 181 segments.insert(0, "".to_string()); 182 segments.push(" ".to_string()); 183 } 184 185 segments.join("\n") 186} 187 188// Get's the subcommand section of a completion file 189// This looks roughly like: 190// 191// case $state in 192// ([bin_name]_args) 193// curcontext=\"${curcontext%:*:*}:[name_hyphen]-command-$words[1]:\" 194// case $line[1] in 195// 196// ([name]) 197// _arguments -C -s -S \ 198// [subcommand_args] 199// && ret=0 200// 201// [RECURSIVE_CALLS] 202// 203// ;;", 204// 205// [repeat] 206// 207// esac 208// ;; 209// esac", 210// 211// Where the following variables are present: 212// [name] = The subcommand name in the form of "install" for "rustup toolchain install" 213// [bin_name] = The full space delineated bin_name such as "rustup toolchain install" 214// [name_hyphen] = The full space delineated bin_name, but replace spaces with hyphens 215// [repeat] = From the same recursive calls, but for all subcommands 216// [subcommand_args] = The same as zsh::get_args_of 217fn get_subcommands_of(parent: &Command) -> String { 218 debug!( 219 "get_subcommands_of: Has subcommands...{:?}", 220 parent.has_subcommands() 221 ); 222 223 if !parent.has_subcommands() { 224 return String::new(); 225 } 226 227 let subcommand_names = utils::subcommands(parent); 228 let mut all_subcommands = vec![]; 229 230 for (ref name, ref bin_name) in &subcommand_names { 231 debug!( 232 "get_subcommands_of:iter: parent={}, name={name}, bin_name={bin_name}", 233 parent.get_name(), 234 ); 235 let mut segments = vec![format!("({name})")]; 236 let subcommand_args = get_args_of( 237 parser_of(parent, bin_name).expect(INTERNAL_ERROR_MSG), 238 Some(parent), 239 ); 240 241 if !subcommand_args.is_empty() { 242 segments.push(subcommand_args); 243 } 244 245 // Get the help text of all child subcommands. 246 let children = get_subcommands_of(parser_of(parent, bin_name).expect(INTERNAL_ERROR_MSG)); 247 248 if !children.is_empty() { 249 segments.push(children); 250 } 251 252 segments.push(String::from(";;")); 253 all_subcommands.push(segments.join("\n")); 254 } 255 256 let parent_bin_name = parent 257 .get_bin_name() 258 .expect("crate::generate should have set the bin_name"); 259 260 format!( 261 " 262 case $state in 263 ({name}) 264 words=($line[{pos}] \"${{words[@]}}\") 265 (( CURRENT += 1 )) 266 curcontext=\"${{curcontext%:*:*}}:{name_hyphen}-command-$line[{pos}]:\" 267 case $line[{pos}] in 268 {subcommands} 269 esac 270 ;; 271esac", 272 name = parent.get_name(), 273 name_hyphen = parent_bin_name.replace(' ', "-"), 274 subcommands = all_subcommands.join("\n"), 275 pos = parent.get_positionals().count() + 1 276 ) 277} 278 279// Get the Command for a given subcommand tree. 280// 281// Given the bin_name "a b c" and the Command for "a" this returns the "c" Command. 282// Given the bin_name "a b c" and the Command for "b" this returns the "c" Command. 283fn parser_of<'cmd>(parent: &'cmd Command, bin_name: &str) -> Option<&'cmd Command> { 284 debug!("parser_of: p={}, bin_name={}", parent.get_name(), bin_name); 285 286 if bin_name == parent.get_bin_name().unwrap_or_default() { 287 return Some(parent); 288 } 289 290 for subcommand in parent.get_subcommands() { 291 if let Some(ret) = parser_of(subcommand, bin_name) { 292 return Some(ret); 293 } 294 } 295 296 None 297} 298 299// Writes out the args section, which ends up being the flags, opts and positionals, and a jump to 300// another ZSH function if there are subcommands. 301// The structure works like this: 302// ([conflicting_args]) [multiple] arg [takes_value] [[help]] [: :(possible_values)] 303// ^-- list '-v -h' ^--'*' ^--'+' ^-- list 'one two three' 304// 305// An example from the rustup command: 306// 307// _arguments -C -s -S \ 308// '(-h --help --verbose)-v[Enable verbose output]' \ 309// '(-V -v --version --verbose --help)-h[Print help information]' \ 310// # ... snip for brevity 311// ':: :_rustup_commands' \ # <-- displays subcommands 312// '*::: :->rustup' \ # <-- displays subcommand args and child subcommands 313// && ret=0 314// 315// The args used for _arguments are as follows: 316// -C: modify the $context internal variable 317// -s: Allow stacking of short args (i.e. -a -b -c => -abc) 318// -S: Do not complete anything after '--' and treat those as argument values 319fn get_args_of(parent: &Command, p_global: Option<&Command>) -> String { 320 debug!("get_args_of"); 321 322 let mut segments = vec![String::from("_arguments \"${_arguments_options[@]}\" \\")]; 323 let opts = write_opts_of(parent, p_global); 324 let flags = write_flags_of(parent, p_global); 325 let positionals = write_positionals_of(parent); 326 327 if !opts.is_empty() { 328 segments.push(opts); 329 } 330 331 if !flags.is_empty() { 332 segments.push(flags); 333 } 334 335 if !positionals.is_empty() { 336 segments.push(positionals); 337 } 338 339 if parent.has_subcommands() { 340 let parent_bin_name = parent 341 .get_bin_name() 342 .expect("crate::generate should have set the bin_name"); 343 let subcommand_bin_name = format!( 344 "\":: :_{name}_commands\" \\", 345 name = parent_bin_name.replace(' ', "__") 346 ); 347 segments.push(subcommand_bin_name); 348 349 let subcommand_text = format!("\"*::: :->{name}\" \\", name = parent.get_name()); 350 segments.push(subcommand_text); 351 }; 352 353 segments.push(String::from("&& ret=0")); 354 segments.join("\n") 355} 356 357// Uses either `possible_vals` or `value_hint` to give hints about possible argument values 358fn value_completion(arg: &Arg) -> Option<String> { 359 if let Some(values) = crate::generator::utils::possible_values(arg) { 360 if values 361 .iter() 362 .any(|value| !value.is_hide_set() && value.get_help().is_some()) 363 { 364 Some(format!( 365 "(({}))", 366 values 367 .iter() 368 .filter_map(|value| { 369 if value.is_hide_set() { 370 None 371 } else { 372 Some(format!( 373 r#"{name}\:"{tooltip}""#, 374 name = escape_value(value.get_name()), 375 tooltip = 376 escape_help(&value.get_help().unwrap_or_default().to_string()), 377 )) 378 } 379 }) 380 .collect::<Vec<_>>() 381 .join("\n") 382 )) 383 } else { 384 Some(format!( 385 "({})", 386 values 387 .iter() 388 .filter(|pv| !pv.is_hide_set()) 389 .map(|n| n.get_name()) 390 .collect::<Vec<_>>() 391 .join(" ") 392 )) 393 } 394 } else { 395 // NB! If you change this, please also update the table in `ValueHint` documentation. 396 Some( 397 match arg.get_value_hint() { 398 ValueHint::Unknown => { 399 return None; 400 } 401 ValueHint::Other => "( )", 402 ValueHint::AnyPath => "_files", 403 ValueHint::FilePath => "_files", 404 ValueHint::DirPath => "_files -/", 405 ValueHint::ExecutablePath => "_absolute_command_paths", 406 ValueHint::CommandName => "_command_names -e", 407 ValueHint::CommandString => "_cmdstring", 408 ValueHint::CommandWithArguments => "_cmdambivalent", 409 ValueHint::Username => "_users", 410 ValueHint::Hostname => "_hosts", 411 ValueHint::Url => "_urls", 412 ValueHint::EmailAddress => "_email_addresses", 413 _ => { 414 return None; 415 } 416 } 417 .to_string(), 418 ) 419 } 420} 421 422/// Escape help string inside single quotes and brackets 423fn escape_help(string: &str) -> String { 424 string 425 .replace('\\', "\\\\") 426 .replace('\'', "'\\''") 427 .replace('[', "\\[") 428 .replace(']', "\\]") 429} 430 431/// Escape value string inside single quotes and parentheses 432fn escape_value(string: &str) -> String { 433 string 434 .replace('\\', "\\\\") 435 .replace('\'', "'\\''") 436 .replace('(', "\\(") 437 .replace(')', "\\)") 438 .replace(' ', "\\ ") 439} 440 441fn write_opts_of(p: &Command, p_global: Option<&Command>) -> String { 442 debug!("write_opts_of"); 443 444 let mut ret = vec![]; 445 446 for o in p.get_opts() { 447 debug!("write_opts_of:iter: o={}", o.get_id()); 448 449 let help = escape_help(&o.get_help().unwrap_or_default().to_string()); 450 let conflicts = arg_conflicts(p, o, p_global); 451 452 let multiple = if let ArgAction::Count | ArgAction::Append = o.get_action() { 453 "*" 454 } else { 455 "" 456 }; 457 458 let vn = match o.get_value_names() { 459 None => " ".to_string(), 460 Some(val) => val[0].to_string(), 461 }; 462 let vc = match value_completion(o) { 463 Some(val) => format!(":{vn}:{val}"), 464 None => format!(":{vn}: "), 465 }; 466 let vc = vc.repeat(o.get_num_args().expect("built").min_values()); 467 468 if let Some(shorts) = o.get_short_and_visible_aliases() { 469 for short in shorts { 470 let s = format!( 471 "'{conflicts}{multiple}-{arg}+[{help}]{value_completion}' \\", 472 conflicts = conflicts, 473 multiple = multiple, 474 arg = short, 475 value_completion = vc, 476 help = help 477 ); 478 479 debug!("write_opts_of:iter: Wrote...{}", &*s); 480 ret.push(s); 481 } 482 } 483 if let Some(longs) = o.get_long_and_visible_aliases() { 484 for long in longs { 485 let l = format!( 486 "'{conflicts}{multiple}--{arg}=[{help}]{value_completion}' \\", 487 conflicts = conflicts, 488 multiple = multiple, 489 arg = long, 490 value_completion = vc, 491 help = help 492 ); 493 494 debug!("write_opts_of:iter: Wrote...{}", &*l); 495 ret.push(l); 496 } 497 } 498 } 499 500 ret.join("\n") 501} 502 503fn arg_conflicts(cmd: &Command, arg: &Arg, app_global: Option<&Command>) -> String { 504 fn push_conflicts(conflicts: &[&Arg], res: &mut Vec<String>) { 505 for conflict in conflicts { 506 if let Some(s) = conflict.get_short() { 507 res.push(format!("-{s}")); 508 } 509 510 if let Some(l) = conflict.get_long() { 511 res.push(format!("--{l}")); 512 } 513 } 514 } 515 516 let mut res = vec![]; 517 match (app_global, arg.is_global_set()) { 518 (Some(x), true) => { 519 let conflicts = x.get_arg_conflicts_with(arg); 520 521 if conflicts.is_empty() { 522 return String::new(); 523 } 524 525 push_conflicts(&conflicts, &mut res); 526 } 527 (_, _) => { 528 let conflicts = cmd.get_arg_conflicts_with(arg); 529 530 if conflicts.is_empty() { 531 return String::new(); 532 } 533 534 push_conflicts(&conflicts, &mut res); 535 } 536 }; 537 538 format!("({})", res.join(" ")) 539} 540 541fn write_flags_of(p: &Command, p_global: Option<&Command>) -> String { 542 debug!("write_flags_of;"); 543 544 let mut ret = vec![]; 545 546 for f in utils::flags(p) { 547 debug!("write_flags_of:iter: f={}", f.get_id()); 548 549 let help = escape_help(&f.get_help().unwrap_or_default().to_string()); 550 let conflicts = arg_conflicts(p, &f, p_global); 551 552 let multiple = if let ArgAction::Count | ArgAction::Append = f.get_action() { 553 "*" 554 } else { 555 "" 556 }; 557 558 if let Some(short) = f.get_short() { 559 let s = format!( 560 "'{conflicts}{multiple}-{arg}[{help}]' \\", 561 multiple = multiple, 562 conflicts = conflicts, 563 arg = short, 564 help = help 565 ); 566 567 debug!("write_flags_of:iter: Wrote...{}", &*s); 568 569 ret.push(s); 570 571 if let Some(short_aliases) = f.get_visible_short_aliases() { 572 for alias in short_aliases { 573 let s = format!("'{conflicts}{multiple}-{alias}[{help}]' \\",); 574 575 debug!("write_flags_of:iter: Wrote...{}", &*s); 576 577 ret.push(s); 578 } 579 } 580 } 581 582 if let Some(long) = f.get_long() { 583 let l = format!( 584 "'{conflicts}{multiple}--{arg}[{help}]' \\", 585 conflicts = conflicts, 586 multiple = multiple, 587 arg = long, 588 help = help 589 ); 590 591 debug!("write_flags_of:iter: Wrote...{}", &*l); 592 593 ret.push(l); 594 595 if let Some(aliases) = f.get_visible_aliases() { 596 for alias in aliases { 597 let l = format!( 598 "'{conflicts}{multiple}--{arg}[{help}]' \\", 599 conflicts = conflicts, 600 multiple = multiple, 601 arg = alias, 602 help = help 603 ); 604 605 debug!("write_flags_of:iter: Wrote...{}", &*l); 606 607 ret.push(l); 608 } 609 } 610 } 611 } 612 613 ret.join("\n") 614} 615 616fn write_positionals_of(p: &Command) -> String { 617 debug!("write_positionals_of;"); 618 619 let mut ret = vec![]; 620 621 // Completions for commands that end with two Vec arguments require special care. 622 // - You can have two Vec args separated with a custom value terminator. 623 // - You can have two Vec args with the second one set to last (raw sets last) 624 // which will require a '--' separator to be used before the second argument 625 // on the command-line. 626 // 627 // We use the '-S' _arguments option to disable completion after '--'. Thus, the 628 // completion for the second argument in scenario (B) does not need to be emitted 629 // because it is implicitly handled by the '-S' option. 630 // We only need to emit the first catch-all. 631 // 632 // Have we already emitted a catch-all multi-valued positional argument 633 // without a custom value terminator? 634 let mut catch_all_emitted = false; 635 636 for arg in p.get_positionals() { 637 debug!("write_positionals_of:iter: arg={}", arg.get_id()); 638 639 let num_args = arg.get_num_args().expect("built"); 640 let is_multi_valued = num_args.max_values() > 1; 641 642 if catch_all_emitted && (arg.is_last_set() || is_multi_valued) { 643 // This is the final argument and it also takes multiple arguments. 644 // We've already emitted a catch-all positional argument so we don't need 645 // to emit anything for this argument because it is implicitly handled by 646 // the use of the '-S' _arguments option. 647 continue; 648 } 649 650 let cardinality_value; 651 let cardinality = if is_multi_valued { 652 match arg.get_value_terminator() { 653 Some(terminator) => { 654 cardinality_value = format!("*{}:", escape_value(terminator)); 655 cardinality_value.as_str() 656 } 657 None => { 658 catch_all_emitted = true; 659 "*:" 660 } 661 } 662 } else if !arg.is_required_set() { 663 ":" 664 } else { 665 "" 666 }; 667 668 let a = format!( 669 "'{cardinality}:{name}{help}:{value_completion}' \\", 670 cardinality = cardinality, 671 name = arg.get_id(), 672 help = arg 673 .get_help() 674 .map(|s| s.to_string()) 675 .map_or("".to_owned(), |v| " -- ".to_owned() + &v) 676 .replace('[', "\\[") 677 .replace(']', "\\]") 678 .replace('\'', "'\\''") 679 .replace(':', "\\:"), 680 value_completion = value_completion(arg).unwrap_or_default() 681 ); 682 683 debug!("write_positionals_of:iter: Wrote...{}", a); 684 685 ret.push(a); 686 } 687 688 ret.join("\n") 689} 690