Merge pull request #4482 from epage/suggest

feat(parser): Show available subcommands when one is missing
This commit is contained in:
Ed Page 2022-11-15 10:40:16 -06:00 committed by GitHub
commit 8cefdf31cc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 52 additions and 5 deletions

View file

@ -9,6 +9,8 @@ pub enum ContextKind {
InvalidArg,
/// Existing arguments
PriorArg,
/// Accepted subcommands
ValidSubcommand,
/// Accepted values
ValidValue,
/// Rejected values
@ -44,6 +46,7 @@ impl ContextKind {
Self::InvalidSubcommand => Some("Invalid Subcommand"),
Self::InvalidArg => Some("Invalid Argument"),
Self::PriorArg => Some("Prior Argument"),
Self::ValidSubcommand => Some("Value Subcommand"),
Self::ValidValue => Some("Value Value"),
Self::InvalidValue => Some("Invalid Value"),
Self::ActualNumValues => Some("Actual Number of Values"),

View file

@ -227,6 +227,24 @@ fn write_dynamic_context(error: &crate::error::Error, styled: &mut StyledStr) ->
styled.none("'");
styled.warning(invalid_sub);
styled.none("' requires a subcommand but one was not provided");
let possible_values = error.get(ContextKind::ValidSubcommand);
if let Some(ContextValue::Strings(possible_values)) = possible_values {
if !possible_values.is_empty() {
styled.none("\n");
styled.none(TAB);
styled.none("[subcommands: ");
if let Some((last, elements)) = possible_values.split_last() {
for v in elements {
styled.good(escape(v));
styled.none(", ");
}
styled.good(escape(last));
}
styled.none("]");
}
}
true
} else {
false

View file

@ -504,17 +504,21 @@ impl<F: ErrorFormatter> Error<F> {
pub(crate) fn missing_subcommand(
cmd: &Command,
name: String,
parent: String,
available: Vec<String>,
usage: Option<StyledStr>,
) -> Self {
let mut err = Self::new(ErrorKind::MissingSubcommand).with_cmd(cmd);
#[cfg(feature = "error-context")]
{
err = err.extend_context_unchecked([(
ContextKind::InvalidSubcommand,
ContextValue::String(name),
)]);
err = err.extend_context_unchecked([
(ContextKind::InvalidSubcommand, ContextValue::String(parent)),
(
ContextKind::ValidSubcommand,
ContextValue::Strings(available),
),
]);
if let Some(usage) = usage {
err = err
.insert_context_unchecked(ContextKind::Usage, ContextValue::StyledStr(usage));

View file

@ -70,6 +70,10 @@ impl<'cmd> Validator<'cmd> {
return Err(Error::missing_subcommand(
self.cmd,
bn.to_string(),
self.cmd
.all_subcommand_names()
.map(|s| s.to_owned())
.collect::<Vec<_>>(),
Usage::new(self.cmd)
.required(&self.required)
.create_usage_with_title(&[]),

View file

@ -58,6 +58,24 @@ fn sub_command_required() {
assert_eq!(err.kind(), ErrorKind::MissingSubcommand);
}
#[test]
#[cfg(feature = "error-context")]
fn sub_command_required_error() {
static ERROR: &str = "\
error: 'sc_required' requires a subcommand but one was not provided
[subcommands: sub1, help]
Usage: sc_required <COMMAND>
For more information try '--help'
";
let cmd = Command::new("sc_required")
.subcommand_required(true)
.subcommand(Command::new("sub1"));
utils::assert_output(cmd, "sc_required", ERROR, true);
}
#[test]
fn arg_required_else_help() {
let result = Command::new("arg_required")