Repository: clap-rs/clap
Pull request #4482 (2022-11-15 16:19:29)

feat(parser): Show available subcommands when one is missing
Similar to
- Listing all required arguments when one is missing
- Listing all possible values when no value is provided

This came up when discussing #3572


Patch:
diff --git a/src/error/context.rs b/src/error/context.rs
index 5cc31097..cd8e308e 100644
--- a/src/error/context.rs
+++ b/src/error/context.rs
@@ -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"),
diff --git a/src/error/format.rs b/src/error/format.rs
index d6edf31c..17149439 100644
--- a/src/error/format.rs
+++ b/src/error/format.rs
@@ -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
diff --git a/src/error/mod.rs b/src/error/mod.rs
index af65529f..d88df46c 100644
--- a/src/error/mod.rs
+++ b/src/error/mod.rs
@@ -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));
diff --git a/src/parser/validator.rs b/src/parser/validator.rs
index 970804bc..0d97c2f6 100644
--- a/src/parser/validator.rs
+++ b/src/parser/validator.rs
@@ -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(&[]),


Test patch:
diff --git a/tests/builder/app_settings.rs b/tests/builder/app_settings.rs
index c9c8cf5a..8cf8b27f 100644
--- a/tests/builder/app_settings.rs
+++ b/tests/builder/app_settings.rs
@@ -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")