-
Notifications
You must be signed in to change notification settings - Fork 2.9k
feat(help): display manpage for nested commands #16432
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
237e35f
649fe9a
2b981c7
296050e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
epage marked this conversation as resolved.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about completions?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can clap complete
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Hmm, just realized we might not be offering completions for built-in
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
What is this?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The original clap completion system auto-generated completion files files similar to what we hand write in cargo. The new completion system is what Cargo is migrating to and what you wrote code for.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah thanks. I thought |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,11 @@ | ||
| use crate::aliased_command; | ||
| use crate::command_prelude::*; | ||
|
|
||
| use cargo::drop_println; | ||
| use cargo::util::errors::CargoResult; | ||
| use cargo_util::paths::resolve_executable; | ||
| use flate2::read::GzDecoder; | ||
|
|
||
| use std::ffi::OsStr; | ||
| use std::ffi::OsString; | ||
| use std::io::Read; | ||
|
|
@@ -15,56 +17,100 @@ const COMPRESSED_MAN: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/man.tgz" | |
| pub fn cli() -> Command { | ||
| subcommand("help") | ||
| .about("Displays help for a cargo command") | ||
| .arg(Arg::new("COMMAND").action(ArgAction::Set).add( | ||
| clap_complete::ArgValueCandidates::new(|| { | ||
| super::builtin() | ||
| .iter() | ||
| .map(|cmd| { | ||
| let name = cmd.get_name(); | ||
| clap_complete::CompletionCandidate::new(name) | ||
| .help(cmd.get_about().cloned()) | ||
| .hide(cmd.is_hide_set()) | ||
| }) | ||
| .collect() | ||
| }), | ||
| )) | ||
| .arg( | ||
| Arg::new("COMMAND") | ||
| .num_args(1..) | ||
| .action(ArgAction::Append) | ||
| .add(clap_complete::ArgValueCandidates::new( | ||
| get_completion_candidates, | ||
| )), | ||
| ) | ||
| } | ||
|
|
||
| pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult { | ||
| let Some(subcommand) = args.get_one::<String>("COMMAND") else { | ||
| let args_command = args | ||
| .get_many::<String>("COMMAND") | ||
| .map(|vals| vals.map(String::as_str).collect::<Vec<_>>()) | ||
| .unwrap_or_default(); | ||
|
|
||
| if args_command.is_empty() { | ||
| let _ = crate::cli::cli(gctx).print_help(); | ||
| return Ok(()); | ||
| }; | ||
| } | ||
|
|
||
| // Expand alias first | ||
| let subcommand = match aliased_command(gctx, subcommand).ok().flatten() { | ||
| // If this alias is more than a simple subcommand pass-through, show the alias. | ||
| Some(argv) if argv.len() > 1 => { | ||
| let alias = argv.join(" "); | ||
| drop_println!(gctx, "`{}` is aliased to `{}`", subcommand, alias); | ||
| return Ok(()); | ||
| } | ||
| // Otherwise, resolve the alias into its subcommand. | ||
| Some(argv) => { | ||
| // An alias with an empty argv can be created via `"empty-alias" = ""`. | ||
| let first = argv.get(0).map(String::as_str).unwrap_or(subcommand); | ||
| first.to_string() | ||
| let cmd: String; | ||
| let lookup_parts: Vec<&str> = if args_command.len() == 1 { | ||
| // Expand alias first | ||
| let subcommand = args_command.first().unwrap(); | ||
| match aliased_command(gctx, subcommand).ok().flatten() { | ||
| Some(argv) if argv.len() > 1 => { | ||
| // If this alias is more than a simple subcommand pass-through, show the alias. | ||
| let alias = argv.join(" "); | ||
| drop_println!(gctx, "`{}` is aliased to `{}`", subcommand, alias); | ||
|
Comment on lines
+46
to
+49
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would we want to consider support handling an alias for
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a nice idea, though I lean towards leave it for future for user-defined aliases. Built-in aliases are handled and a man page is displayed. |
||
| return Ok(()); | ||
| } | ||
| // Otherwise, resolve the alias into its subcommand. | ||
| Some(argv) => { | ||
| // An alias with an empty argv can be created via `"empty-alias" = ""`. | ||
| cmd = argv | ||
| .into_iter() | ||
| .next() | ||
| .unwrap_or_else(|| subcommand.to_string()); | ||
| vec![cmd.as_str()] | ||
| } | ||
| None => args_command.clone(), | ||
| } | ||
| None => subcommand.to_string(), | ||
| } else { | ||
| args_command.clone() | ||
| }; | ||
|
|
||
| if super::builtin_exec(&subcommand).is_some() { | ||
| if try_help(&subcommand)? { | ||
| return Ok(()); | ||
| match find_builtin_cmd(&lookup_parts) { | ||
| Ok(path) => { | ||
| let man_page_name = path.join("-"); | ||
| if try_help(&man_page_name)? { | ||
| return Ok(()); | ||
| } | ||
| crate::execute_internal_subcommand( | ||
| gctx, | ||
| &[OsStr::new(&man_page_name), OsStr::new("--help")], | ||
| )?; | ||
| } | ||
| Err(FindError::UnknownCommand(cmd)) => { | ||
| if lookup_parts.len() == 1 { | ||
| if let Some(man_page_name) = find_builtin_cmd_dash_joined(cmd) { | ||
| if try_help(&man_page_name)? { | ||
| return Ok(()); | ||
| } | ||
| crate::execute_internal_subcommand( | ||
| gctx, | ||
| &[OsStr::new(&man_page_name), OsStr::new("--help")], | ||
| )?; | ||
| } else { | ||
| crate::execute_external_subcommand( | ||
| gctx, | ||
| cmd, | ||
| &[OsStr::new(cmd), OsStr::new("--help")], | ||
| )?; | ||
| } | ||
| } else { | ||
| let err = anyhow::format_err!( | ||
| "no such command: `{cmd}`\n\n\ | ||
| help: view all installed commands with `cargo --list`", | ||
| ); | ||
| return Err(err.into()); | ||
| } | ||
| } | ||
| Err(FindError::UnknownSubcommand { | ||
| valid_prefix, | ||
| invalid, | ||
| }) => { | ||
| let valid_prefix = valid_prefix.join(" "); | ||
| let err = anyhow::format_err!( | ||
| "no such command: `cargo {valid_prefix} {invalid}` \n\n\ | ||
| help: view available subcommands with `cargo {valid_prefix} --help`", | ||
| ); | ||
| return Err(err.into()); | ||
| } | ||
| crate::execute_internal_subcommand(gctx, &[OsStr::new(&subcommand), OsStr::new("--help")])?; | ||
| } else { | ||
| // If not built-in, try giving `--help` to external command. | ||
| crate::execute_external_subcommand( | ||
| gctx, | ||
| &subcommand, | ||
| &[OsStr::new(&subcommand), OsStr::new("--help")], | ||
| )?; | ||
| } | ||
|
|
||
| Ok(()) | ||
|
|
@@ -141,3 +187,123 @@ fn write_and_spawn(name: &str, contents: &[u8], command: &str) -> CargoResult<() | |
| drop(cmd.wait()); | ||
| Ok(()) | ||
| } | ||
|
|
||
| enum FindError<'a> { | ||
| /// The primary command was not found. | ||
| UnknownCommand(&'a str), | ||
| /// A subcommand in the path was not found. | ||
| UnknownSubcommand { | ||
| valid_prefix: Vec<&'a str>, | ||
| invalid: &'a str, | ||
| }, | ||
| } | ||
|
|
||
| /// Finds a auiltin command. | ||
| fn find_builtin_cmd<'a>(parts: &[&'a str]) -> Result<Vec<String>, FindError<'a>> { | ||
| let Some((first, rest)) = parts.split_first() else { | ||
| return Err(FindError::UnknownCommand("")); | ||
| }; | ||
|
|
||
| let builtins = super::builtin(); | ||
|
|
||
| let Some(mut current) = builtins | ||
| .iter() | ||
| .find(|cmd| cmd.get_name() == *first || cmd.get_all_aliases().any(|a| a == *first)) | ||
| else { | ||
| return Err(FindError::UnknownCommand(first)); | ||
| }; | ||
|
|
||
| let mut path = vec![current.get_name().to_string()]; | ||
|
|
||
| for (i, &part) in rest.iter().enumerate() { | ||
| let next = current | ||
| .get_subcommands() | ||
| .find(|cmd| cmd.get_name() == part || cmd.get_all_aliases().any(|a| a == part)); | ||
| if let Some(next) = next { | ||
| path.push(next.get_name().to_string()); | ||
| current = next; | ||
| } else { | ||
| let valid_prefix = [*first] | ||
| .into_iter() | ||
| .chain(rest[..i].iter().copied()) | ||
| .collect::<Vec<_>>(); | ||
| return Err(FindError::UnknownSubcommand { | ||
| valid_prefix, | ||
| invalid: part, | ||
| }); | ||
| }; | ||
| } | ||
|
|
||
| Ok(path) | ||
| } | ||
|
|
||
| fn find_builtin_cmd_dash_joined(s: &str) -> Option<String> { | ||
| let builtins = super::builtin(); | ||
|
|
||
| for cmd in builtins.iter() { | ||
| if let Some(result) = try_match_cmd(cmd, s) { | ||
| return Some(result); | ||
| } | ||
| } | ||
| None | ||
| } | ||
|
|
||
| /// Tries to match a single dash-joined argument against commands | ||
| fn try_match_cmd(cmd: &Command, arg: &str) -> Option<String> { | ||
| let name = cmd.get_name(); | ||
|
|
||
| if arg == name || cmd.get_all_aliases().any(|alias| alias == arg) { | ||
| return Some(name.to_string()); | ||
| } | ||
|
|
||
| if let Some(rest) = arg.strip_prefix(name).and_then(|r| r.strip_prefix('-')) { | ||
| for cmd in cmd.get_subcommands() { | ||
| if let Some(sub_cmds) = try_match_cmd(cmd, rest) { | ||
| return Some(format!("{name}-{sub_cmds}")); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| for alias in cmd.get_all_aliases() { | ||
| if let Some(rest) = arg.strip_prefix(alias).and_then(|r| r.strip_prefix('-')) { | ||
| for cmd in cmd.get_subcommands() { | ||
| if let Some(sub_cmds) = try_match_cmd(cmd, rest) { | ||
| return Some(format!("{name}-{sub_cmds}")); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| None | ||
| } | ||
|
|
||
| /// Returns dash-joined names for nested commands, | ||
| /// so they can be completed as single tokens. | ||
| fn get_completion_candidates() -> Vec<clap_complete::CompletionCandidate> { | ||
| fn walk( | ||
| cmd: Command, | ||
| prefix: Option<&String>, | ||
| candidates: &mut Vec<clap_complete::CompletionCandidate>, | ||
| ) { | ||
| let name = cmd.get_name(); | ||
| let key = match prefix { | ||
| Some(prefix) => format!("{prefix}-{name}"), | ||
| None => name.to_string(), | ||
| }; | ||
|
|
||
| for cmd in cmd.get_subcommands() { | ||
| walk(cmd.clone(), Some(&key), candidates); | ||
| } | ||
|
|
||
| let candidate = clap_complete::CompletionCandidate::new(&key) | ||
| .help(cmd.get_about().cloned()) | ||
| .hide(cmd.is_hide_set()); | ||
| candidates.push(candidate); | ||
| } | ||
|
|
||
| let mut candidates = Vec::new(); | ||
| for cmd in super::builtin() { | ||
| walk(cmd, None, &mut candidates); | ||
| } | ||
| candidates | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.