Skip to content

Commit

Permalink
Merge pull request #4482 from epage/suggest
Browse files Browse the repository at this point in the history
feat(parser): Show available subcommands when one is missing
  • Loading branch information
epage committed Nov 15, 2022
2 parents 010976c + 6b62c82 commit 8cefdf3
Show file tree
Hide file tree
Showing 5 changed files with 52 additions and 5 deletions.
3 changes: 3 additions & 0 deletions src/error/context.rs
Expand Up @@ -9,6 +9,8 @@ pub enum ContextKind {
InvalidArg,
/// Existing arguments
PriorArg,
/// Accepted subcommands
ValidSubcommand,
/// Accepted values
ValidValue,
/// Rejected values
Expand Down Expand Up @@ -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"),
Expand Down
18 changes: 18 additions & 0 deletions src/error/format.rs
Expand Up @@ -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
Expand Down
14 changes: 9 additions & 5 deletions src/error/mod.rs
Expand Up @@ -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));
Expand Down
4 changes: 4 additions & 0 deletions src/parser/validator.rs
Expand Up @@ -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(&[]),
Expand Down
18 changes: 18 additions & 0 deletions tests/builder/app_settings.rs
Expand Up @@ -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")
Expand Down

0 comments on commit 8cefdf3

Please sign in to comment.