Repository: clap-rs/clap
Pull request #3680 (2022-05-03 19:35:44)

fix(help): Offer a html_template variable for display name
This is a step towards #992.  When help renders the application name, it
uses the `bin` template variable which is just the `bin` name with
spaces converted to ` `.  While having `app.exe sub` makes sense,
`app.exe-sub` does not.

To get around needing this for usage, we've created a `display_name`
field that is fairly similar but
- The root name is the `name` and not `bin_name`
- We always join with `-`

This means that the derived `bin_name` will only show up in usage.

For now, the default template has not been updated as that is a minor
compatibility change and should be in a minor release, at least.  I was
worried this would be a full breaking change.  The main case I was
worried about was cargo subcommands but our tests show they should just
work.

<!--
Thanks for helping out!

Please link the appropriate issue from your PR.

If you don't have an issue, we'd recommend starting with one first so the PR can focus on the
implementation (unless its an obvious bug or documentation fix that will have
little conversation).
-->



Patch:
diff --git a/src/build/command.rs b/src/build/command.rs
index c228487f..5306d95f 100644
--- a/src/build/command.rs
+++ b/src/build/command.rs
@@ -77,6 +77,7 @@ pub struct App<'help> {
     name: String,
     long_flag: Option<&'help str>,
     short_flag: Option<char>,
+    display_name: Option<String>,
     bin_name: Option<String>,
     author: Option<&'help str>,
     version: Option<&'help str>,
@@ -1380,6 +1381,22 @@ impl<'help> App<'help> {
         self
     }
 
+    /// Overrides the runtime-determined display name of the program for help and error messages.
+    ///
+    /// # Examples
+    ///
+    /// ```no_run
+    /// # use clap::Command;
+    /// Command::new("My Program")
+    ///      .display_name("my_program")
+    /// # ;
+    /// ```
+    #[must_use]
+    pub fn display_name<S: Into<String>>(mut self, name: S) -> Self {
+        self.display_name = Some(name.into());
+        self
+    }
+
     /// Sets the author(s) for the help message.
     ///
     /// **Pro-tip:** Use `clap`s convenience macro [`crate_authors!`] to
@@ -3165,6 +3182,12 @@ impl<'help> App<'help> {
         self.usage_name.as_deref()
     }
 
+    /// Get the name of the binary.
+    #[inline]
+    pub fn get_display_name(&self) -> Option<&str> {
+        self.display_name.as_deref()
+    }
+
     /// Get the name of the binary.
     #[inline]
     pub fn get_bin_name(&self) -> Option<&str> {
@@ -4101,6 +4124,7 @@ impl<'help> App<'help> {
                 mid_string.push(' ');
             }
         }
+        let is_multicall_set = cfg!(feature = "unstable-multicall") && self.is_multicall_set();
 
         let sc = self.subcommands.iter_mut().find(|s| s.name == name)?;
 
@@ -4129,12 +4153,40 @@ impl<'help> App<'help> {
 
         // bin_name should be parent's bin_name + [<reqs>] + the sc's name separated by
         // a space
-        sc.bin_name = Some(format!(
+        let bin_name = format!(
             "{}{}{}",
             self.bin_name.as_ref().unwrap_or(&String::new()),
             if self.bin_name.is_some() { " " } else { "" },
             &*sc.name
-        ));
+        );
+        debug!(
+            "Command::_build_subcommand Setting bin_name of {} to {:?}",
+            sc.name, bin_name
+        );
+        sc.bin_name = Some(bin_name);
+
+        if sc.display_name.is_none() {
+            let self_display_name = if is_multicall_set {
+                self.display_name.as_deref().unwrap_or("")
+            } else {
+                self.display_name.as_deref().unwrap_or(&self.name)
+            };
+            let display_name = format!(
+                "{}{}{}",
+                self_display_name,
+                if !self_display_name.is_empty() {
+                    "-"
+                } else {
+                    ""
+                },
+                &*sc.name
+            );
+            debug!(
+                "Command::_build_subcommand Setting display_name of {} to {:?}",
+                sc.name, display_name
+            );
+            sc.display_name = Some(display_name);
+        }
 
         // Ensure all args are built and ready to parse
         sc._build_self();
@@ -4155,14 +4207,14 @@ impl<'help> App<'help> {
                     mid_string.push(' ');
                 }
             }
+            let is_multicall_set = cfg!(feature = "unstable-multicall") && self.is_multicall_set();
 
-            let self_bin_name =
-                if cfg!(feature = "unstable-multicall") && self.is_multicall_set() {
-                    self.bin_name.as_deref().unwrap_or("")
-                } else {
-                    self.bin_name.as_deref().unwrap_or(&self.name)
-                }
-                .to_owned();
+            let self_bin_name = if is_multicall_set {
+                self.bin_name.as_deref().unwrap_or("")
+            } else {
+                self.bin_name.as_deref().unwrap_or(&self.name)
+            }
+            .to_owned();
 
             for mut sc in &mut self.subcommands {
                 debug!("Command::_build_bin_names:iter: bin_name set...");
@@ -4216,6 +4268,35 @@ impl<'help> App<'help> {
                         sc.name, sc.bin_name
                     );
                 }
+
+                if sc.display_name.is_none() {
+                    let self_display_name = if is_multicall_set {
+                        self.display_name.as_deref().unwrap_or("")
+                    } else {
+                        self.display_name.as_deref().unwrap_or(&self.name)
+                    };
+                    let display_name = format!(
+                        "{}{}{}",
+                        self_display_name,
+                        if !self_display_name.is_empty() {
+                            "-"
+                        } else {
+                            ""
+                        },
+                        &*sc.name
+                    );
+                    debug!(
+                        "Command::_build_bin_names:iter: Setting display_name of {} to {:?}",
+                        sc.name, display_name
+                    );
+                    sc.display_name = Some(display_name);
+                } else {
+                    debug!(
+                        "Command::_build_bin_names::iter: Using existing display_name of {} ({:?})",
+                        sc.name, sc.display_name
+                    );
+                }
+
                 sc._build_bin_names_internal();
             }
             self.set(AppSettings::BinNameBuilt);
@@ -4805,6 +4886,7 @@ impl<'help> Default for App<'help> {
             name: Default::default(),
             long_flag: Default::default(),
             short_flag: Default::default(),
+            display_name: Default::default(),
             bin_name: Default::default(),
             author: Default::default(),
             version: Default::default(),
diff --git a/src/output/help.rs b/src/output/help.rs
index 03221686..9bb21cd4 100644
--- a/src/output/help.rs
+++ b/src/output/help.rs
@@ -952,6 +952,22 @@ impl<'help, 'cmd, 'writer> Help<'help, 'cmd, 'writer> {
         Ok(())
     }
 
+    /// Writes binary name of a Parser Object to the wrapped stream.
+    fn write_display_name(&mut self) -> io::Result<()> {
+        debug!("Help::write_display_name");
+
+        let display_name = text_wrapper(
+            &self
+                .cmd
+                .get_display_name()
+                .unwrap_or_else(|| self.cmd.get_name())
+                .replace("{n}", "\n"),
+            self.term_w,
+        );
+        self.good(&display_name)?;
+        Ok(())
+    }
+
     /// Writes binary name of a Parser Object to the wrapped stream.
     fn write_bin_name(&mut self) -> io::Result<()> {
         debug!("Help::write_bin_name");
@@ -1019,6 +1035,9 @@ impl<'help, 'cmd, 'writer> Help<'help, 'cmd, 'writer> {
         for part in parts {
             tags! {
                 match part {
+                    "name" => {
+                        self.write_display_name()?;
+                    }
                     "bin" => {
                         self.write_bin_name()?;
                     }


Test patch:
diff --git a/tests/builder/help.rs b/tests/builder/help.rs
index 1173f19a..13a7e5d5 100644
--- a/tests/builder/help.rs
+++ b/tests/builder/help.rs
@@ -2737,3 +2737,43 @@ OPTIONS:
     subcmd.write_help(&mut buf).unwrap();
     utils::assert_eq(EXPECTED, String::from_utf8(buf).unwrap());
 }
+
+#[test]
+fn display_name_default() {
+    let mut cmd = Command::new("app").bin_name("app.exe");
+    cmd.build();
+    assert_eq!(cmd.get_display_name(), None);
+}
+
+#[test]
+fn display_name_explicit() {
+    let mut cmd = Command::new("app")
+        .bin_name("app.exe")
+        .display_name("app.display");
+    cmd.build();
+    assert_eq!(cmd.get_display_name(), Some("app.display"));
+}
+
+#[test]
+fn display_name_subcommand_default() {
+    let mut cmd = Command::new("parent").subcommand(Command::new("child").bin_name("child.exe"));
+    cmd.build();
+    assert_eq!(
+        cmd.find_subcommand("child").unwrap().get_display_name(),
+        Some("parent-child")
+    );
+}
+
+#[test]
+fn display_name_subcommand_explicit() {
+    let mut cmd = Command::new("parent").subcommand(
+        Command::new("child")
+            .bin_name("child.exe")
+            .display_name("child.display"),
+    );
+    cmd.build();
+    assert_eq!(
+        cmd.find_subcommand("child").unwrap().get_display_name(),
+        Some("child.display")
+    );
+}