Skip to content
Merged
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
246 changes: 206 additions & 40 deletions src/bin/cargo/commands/help.rs
Comment thread
epage marked this conversation as resolved.
Comment thread
epage marked this conversation as resolved.
Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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,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;
Expand All @@ -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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Would we want to consider support handling an alias for cargo report future-incompatibilities rather than just printing the alias definition as part of this work?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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(())
Expand Down Expand Up @@ -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
}
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
22 changes: 21 additions & 1 deletion src/doc/man/cargo-help.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ cargo-help --- Get help for a Cargo command

Prints a help message for the given command.

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

Spaces separate hierarchy levels only between a parent command and its
subcommands. Dashes that are part of a command's name, such as
`generate-lockfile`, must always be preserved.

Multiple command levels can also be written as a single dash-joined word.
For example, `cargo help report-future-incompatibilities` is equivalent to
`cargo help report future-incompatibilities`.

## OPTIONS

### Display Options
Expand All @@ -38,7 +50,15 @@ Prints a help message for the given 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
22 changes: 21 additions & 1 deletion src/doc/man/generated_txt/cargo-help.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ SYNOPSIS
DESCRIPTION
Prints a help message for the given command.

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

Spaces separate hierarchy levels only between a parent command and its
subcommands. Dashes that are part of a command’s name, such as
generate-lockfile, must always be preserved.

Multiple command levels can also be written as a single dash-joined
word. For example, cargo help report-future-incompatibilities is
equivalent to cargo help report future-incompatibilities.

OPTIONS
Display Options
-v, --verbose
Expand Down Expand Up @@ -120,7 +132,15 @@ EXAMPLES

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
22 changes: 21 additions & 1 deletion src/doc/src/commands/cargo-help.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ cargo-help --- Get help for a Cargo command

Prints a help message for the given command.

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

Spaces separate hierarchy levels only between a parent command and its
subcommands. Dashes that are part of a command's name, such as
`generate-lockfile`, must always be preserved.

Multiple command levels can also be written as a single dash-joined word.
For example, `cargo help report-future-incompatibilities` is equivalent to
`cargo help report future-incompatibilities`.

## OPTIONS

### Display Options
Expand Down Expand Up @@ -145,7 +157,15 @@ details on environment variables that Cargo reads.

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
Loading