Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .ignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ src/etc/man
!src/doc/src/commands/manifest-commands.md
!src/doc/src/commands/package-commands.md
!src/doc/src/commands/publishing-commands.md
!src/doc/src/commands/report-commands.md

# Snapshots of HTML reports and log files are just too large
tests/testsuite/**/*.jsonl
Expand Down
256 changes: 181 additions & 75 deletions src/bin/cargo/commands/help.rs
Copy link
Member Author

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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about completions?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can clap complete cargo help report fu<tab>? I guess it is not really supported and we might need to auto-complete in dash-joined form.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

num_args(1..) are assumed to not be hierarchical so it can't. We don't have external subcommand completers yet, so switching to that wouldn't help.

Hmm, just realized we might not be offering completions for built-in help with the new completion engine. The old one would instantiate help in special way that automatically generated subcommands for every subcommand, recursively.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with the new completion engine

What is this?

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah thanks. I thought clap_completion going to get a new engine 😆.

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;
Expand All @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cargo --list won't help for cargo report foo

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")?;
Expand All @@ -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.
Expand Down Expand Up @@ -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
}
5 changes: 4 additions & 1 deletion src/bin/cargo/commands/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ pub fn cli() -> Command {
)
.value_name("id"),
)
.arg_package("Package to display a report for"),
.arg_package("Package to display a report for")
.after_help(color_print::cstr!(
"Run `<bright-cyan,bold>cargo help report future-incompatibilities</>` for more detailed information.\n"
)),
)
.subcommand(
subcommand("timings")
Expand Down
41 changes: 40 additions & 1 deletion src/doc/man/cargo-help.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be bothering if these don't do anything

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or we can hide them also in --help text. Either works for me but man page and help text should be aligned.


{{> 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

Expand Down
4 changes: 2 additions & 2 deletions src/doc/man/cargo-remove.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,9 @@ Package to remove from.

cargo remove --dev trybuild

3. Remove `nom` from the `x86_64-pc-windows-gnu` dependencies table
3. Remove `nom` from the `wasm32-unknown-unknown` dependencies table

cargo remove --target x86_64-pc-windows-gnu nom
cargo remove --target wasm32-unknown-unknown nom

## SEE ALSO
{{man "cargo" 1}}, {{man "cargo-add" 1}}
Loading