-
Notifications
You must be signed in to change notification settings - Fork 2.8k
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
base: master
Are you sure you want to change the base?
Changes from all commits
0b21787
ddedfcc
e5aa3db
9c546e2
80b9d3e
a889c91
85ac7ee
588980b
cf7ef9f
86f89b9
26a5d39
da09ff3
7eef672
ba302cc
ec6c69a
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.
Show resolved
Hide 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,12 @@ | ||
| 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::collections::HashMap; | ||
| use std::ffi::OsStr; | ||
| use std::ffi::OsString; | ||
| use std::io::Read; | ||
|
|
@@ -15,82 +18,101 @@ 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 subcommand = args.get_one::<String>("COMMAND"); | ||
| if let Some(subcommand) = subcommand { | ||
| if !try_help(gctx, subcommand)? { | ||
| match check_builtin(&subcommand) { | ||
| Some(s) => { | ||
| crate::execute_internal_subcommand( | ||
| gctx, | ||
| &[OsStr::new(s), OsStr::new("--help")], | ||
| )?; | ||
| } | ||
| None => { | ||
| crate::execute_external_subcommand( | ||
| gctx, | ||
| subcommand, | ||
| &[OsStr::new(subcommand), OsStr::new("--help")], | ||
| )?; | ||
| } | ||
| } | ||
| } | ||
| } else { | ||
| let mut cmd = crate::cli::cli(gctx); | ||
| let _ = cmd.print_help(); | ||
| 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(()); | ||
| } | ||
| Ok(()) | ||
| } | ||
|
|
||
| fn try_help(gctx: &GlobalContext, subcommand: &str) -> CargoResult<bool> { | ||
| let subcommand = match check_alias(gctx, subcommand) { | ||
| // 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(true); | ||
| let subcommand = 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); | ||
| 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() | ||
| } | ||
| None => subcommand.to_string(), | ||
| } | ||
| // 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() | ||
| } else { | ||
| if !is_valid_builtin_command_path(&args_command) { | ||
| let command_str = args_command.join(" "); | ||
| let err = anyhow::format_err!( | ||
| "no such command: `{command_str}`\n\n\ | ||
| help: view all installed commands with `cargo --list`", | ||
| ); | ||
| return Err(err.into()); | ||
| } | ||
|
Comment on lines
+61
to
68
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.
|
||
| None => subcommand.to_string(), | ||
|
|
||
| args_command.join("-") | ||
| }; | ||
|
|
||
| let subcommand = match check_builtin(&subcommand) { | ||
| Some(s) => s, | ||
| None => return Ok(false), | ||
| let builtins = all_builtin_commands(); | ||
| let Some(lookup) = builtins.get(&subcommand).cloned() else { | ||
| // If not built-in, try giving `--help` to external command. | ||
| crate::execute_external_subcommand( | ||
| gctx, | ||
| &subcommand, | ||
| &[OsStr::new(&subcommand), OsStr::new("--help")], | ||
| )?; | ||
|
|
||
| return Ok(()); | ||
| }; | ||
|
|
||
| let subcommand = match lookup { | ||
| ManPageLookup::Direct => subcommand, | ||
| ManPageLookup::RedirectTo(primary) => primary, | ||
| }; | ||
|
|
||
| if resolve_executable(Path::new("man")).is_ok() { | ||
| let man = match extract_man(subcommand, "1") { | ||
| Some(man) => man, | ||
| None => return Ok(false), | ||
| if try_help(&subcommand)? { | ||
| return Ok(()); | ||
| } | ||
|
|
||
| crate::execute_internal_subcommand(gctx, &[OsStr::new(&subcommand), OsStr::new("--help")])?; | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| fn try_help(subcommand: &str) -> CargoResult<bool> { | ||
| // ALLOWED: For testing cargo itself only. | ||
| #[allow(clippy::disallowed_methods)] | ||
| let force_help_text = std::env::var("__CARGO_TEST_FORCE_HELP_TXT").is_ok(); | ||
|
|
||
| if resolve_executable(Path::new("man")).is_ok() && !force_help_text { | ||
| let Some(man) = extract_man(subcommand, "1") else { | ||
| return Ok(false); | ||
| }; | ||
| write_and_spawn(subcommand, &man, "man")?; | ||
| } else { | ||
| let txt = match extract_man(subcommand, "txt") { | ||
| Some(txt) => txt, | ||
| None => return Ok(false), | ||
| let Some(txt) = extract_man(subcommand, "txt") else { | ||
| return Ok(false); | ||
| }; | ||
| if resolve_executable(Path::new("less")).is_ok() { | ||
| if force_help_text { | ||
| drop(std::io::stdout().write_all(&txt)); | ||
| } else if resolve_executable(Path::new("less")).is_ok() { | ||
| write_and_spawn(subcommand, &txt, "less")?; | ||
| } else if resolve_executable(Path::new("more")).is_ok() { | ||
| write_and_spawn(subcommand, &txt, "more")?; | ||
|
|
@@ -101,20 +123,6 @@ fn try_help(gctx: &GlobalContext, subcommand: &str) -> CargoResult<bool> { | |
| Ok(true) | ||
| } | ||
|
|
||
| /// Checks if the given subcommand is an alias. | ||
| /// | ||
| /// Returns None if it is not an alias. | ||
| fn check_alias(gctx: &GlobalContext, subcommand: &str) -> Option<Vec<String>> { | ||
| aliased_command(gctx, subcommand).ok().flatten() | ||
| } | ||
|
|
||
| /// Checks if the given subcommand is a built-in command (not via an alias). | ||
| /// | ||
| /// Returns None if it is not a built-in command. | ||
| fn check_builtin(subcommand: &str) -> Option<&str> { | ||
| super::builtin_exec(subcommand).map(|_| subcommand) | ||
| } | ||
|
|
||
| /// Extracts the given man page from the compressed archive. | ||
| /// | ||
| /// Returns None if the command wasn't found. | ||
|
|
@@ -157,3 +165,101 @@ fn write_and_spawn(name: &str, contents: &[u8], command: &str) -> CargoResult<() | |
| drop(cmd.wait()); | ||
| Ok(()) | ||
| } | ||
|
|
||
| #[derive(Clone)] | ||
| enum ManPageLookup { | ||
| /// For primary command name, use it directly for man page lookup | ||
| Direct, | ||
| /// For alias, redirect to the primary command name for man page lookup | ||
| RedirectTo(String), | ||
| } | ||
|
|
||
| /// Validates that multi-arg paths represent actual nested commands | ||
| fn is_valid_builtin_command_path(parts: &[&str]) -> bool { | ||
| let Some((first, remainings)) = parts.split_first() else { | ||
| return false; | ||
| }; | ||
|
|
||
| let builtins = super::builtin(); | ||
|
|
||
| let Some(mut current) = builtins.iter().find(|cmd| cmd.get_name() == *first) else { | ||
| return false; | ||
| }; | ||
|
|
||
| for &part in remainings { | ||
| let next = current | ||
| .get_subcommands() | ||
| .find(|cmd| cmd.get_name() == part || cmd.get_all_aliases().any(|a| a == part)); | ||
| let Some(next) = next else { | ||
| return false; | ||
| }; | ||
| current = next; | ||
| } | ||
|
|
||
| true | ||
| } | ||
|
|
||
| /// Builds a map of all command names (including nested and aliases) to their man page lookup. | ||
| fn all_builtin_commands() -> HashMap<String, ManPageLookup> { | ||
| fn walk(cmd: Command, prefix: Option<&String>, map: &mut HashMap<String, ManPageLookup>) { | ||
| 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), map); | ||
| } | ||
|
|
||
| for alias in cmd.get_all_aliases() { | ||
| let alias_key = match prefix { | ||
| Some(prefix) => format!("{prefix}-{alias}"), | ||
| None => alias.to_string(), | ||
| }; | ||
| // Register aliases as redirects to the primary command | ||
| map.insert(alias_key, ManPageLookup::RedirectTo(key.clone())); | ||
| } | ||
|
|
||
| // Register the primary command name | ||
| map.insert(key, ManPageLookup::Direct); | ||
| } | ||
|
|
||
| let mut map = HashMap::new(); | ||
| for cmd in super::builtin() { | ||
| walk(cmd, None, &mut map); | ||
| } | ||
|
|
||
| map | ||
| } | ||
|
|
||
| /// 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 | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,13 +12,52 @@ cargo-help --- Get help for a Cargo command | |
|
|
||
| Prints a help message for the given command. | ||
|
|
||
| For nested commands (commands with subcommands), separate the command levels | ||
| with spaces. For example, `cargo help report future-incompatibilities` displays | ||
| help for the `cargo report future-incompatibilities` command. | ||
|
|
||
| The dash-joined form (`cargo help report-future-incompatibilities`) also works | ||
| for compatibility. | ||
|
|
||
| Note that spaces only separate hierarchy levels between a parent command and its | ||
| subcommands. Dashes that are part of a command's name (like `generate-lockfile`) | ||
| must always be preserved. | ||
|
|
||
| ## OPTIONS | ||
|
|
||
| ### Display Options | ||
|
|
||
| {{#options}} | ||
| {{> options-display }} | ||
| {{/options}} | ||
|
|
||
| ### Manifest Options | ||
|
|
||
| {{#options}} | ||
| {{> options-locked }} | ||
| {{/options}} | ||
|
Comment on lines
+34
to
+38
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. Should we be bothering if these don't do anything
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. Or we can hide them also in |
||
|
|
||
| {{> section-options-common }} | ||
|
|
||
| {{> section-environment }} | ||
|
|
||
| {{> section-exit-status }} | ||
|
|
||
| ## EXAMPLES | ||
|
|
||
| 1. Get help for a command: | ||
|
|
||
| cargo help build | ||
|
|
||
| 2. Help is also available with the `--help` flag: | ||
| 2. Get help for a nested command: | ||
|
|
||
| cargo help report future-incompatibilities | ||
|
|
||
| 3. The dash-joined form also works: | ||
|
|
||
| cargo help report-future-incompatibilities | ||
|
|
||
| 4. Help is also available with the `--help` flag: | ||
|
|
||
| cargo build --help | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will need an team FCP