From 936aea351a22ac86494e09b499dff9eb41b1ceeb Mon Sep 17 00:00:00 2001 From: Josh Triplett Date: Tue, 4 Oct 2022 01:56:19 +0100 Subject: [PATCH] feat: Support visible aliases for possible values Show visible aliases in help and in generated manpages. Does not show aliases in error messages yet. --- clap_mangen/src/render.rs | 8 ++- clap_mangen/tests/common.rs | 5 +- .../tests/snapshots/possible_values.bash.roff | 2 +- examples/tutorial_derive/04_01_enum.md | 8 ++- examples/tutorial_derive/04_01_enum.rs | 1 + src/builder/possible_value.rs | 72 ++++++++++++++++--- src/output/help_template.rs | 41 +++++++++-- tests/derive/help.rs | 3 +- 8 files changed, 120 insertions(+), 20 deletions(-) diff --git a/clap_mangen/src/render.rs b/clap_mangen/src/render.rs index e4b09f567b5..c7f53d987ec 100644 --- a/clap_mangen/src/render.rs +++ b/clap_mangen/src/render.rs @@ -339,9 +339,13 @@ fn format_possible_values(possibles: &Vec<&clap::builder::PossibleValue>) -> (Ve if with_help { for value in possibles { let val_name = value.get_name(); + let mut visible_aliases = value.get_quoted_visible_aliases().join(", "); + if !visible_aliases.is_empty() { + visible_aliases = format!(" (alias: {visible_aliases})"); + } match value.get_help() { - Some(help) => lines.push(format!("{}: {}", val_name, help)), - None => lines.push(val_name.to_string()), + Some(help) => lines.push(format!("{val_name}: {help}{visible_aliases}")), + None => lines.push(format!("{val_name}{visible_aliases}")), } } } else { diff --git a/clap_mangen/tests/common.rs b/clap_mangen/tests/common.rs index ca9f1f9b4fb..8eee263a50c 100644 --- a/clap_mangen/tests/common.rs +++ b/clap_mangen/tests/common.rs @@ -294,7 +294,10 @@ pub fn possible_values_command(name: &'static str) -> clap::Command { .long("method") .action(clap::ArgAction::Set) .value_parser([ - PossibleValue::new("fast").help("use the Fast method"), + PossibleValue::new("fast") + .help("use the Fast method") + .visible_alias("quick") + .alias("zippy"), PossibleValue::new("slow").help("use the slow method"), PossibleValue::new("normal") .help("use normal mode") diff --git a/clap_mangen/tests/snapshots/possible_values.bash.roff b/clap_mangen/tests/snapshots/possible_values.bash.roff index d4be2d2c9af..de31b1e0a58 100644 --- a/clap_mangen/tests/snapshots/possible_values.bash.roff +++ b/clap_mangen/tests/snapshots/possible_values.bash.roff @@ -19,7 +19,7 @@ my/-app /fIPossible values:/fR .RS 14 .IP /(bu 2 -fast: use the Fast method +fast: use the Fast method (alias: quick) .IP /(bu 2 slow: use the slow method .RE diff --git a/examples/tutorial_derive/04_01_enum.md b/examples/tutorial_derive/04_01_enum.md index f6f397798e1..027d86f814c 100644 --- a/examples/tutorial_derive/04_01_enum.md +++ b/examples/tutorial_derive/04_01_enum.md @@ -5,7 +5,7 @@ A simple to use, efficient, and full-featured Command Line Argument Parser Usage: 04_01_enum_derive[EXE] Arguments: - What mode to run the program in [possible values: fast, slow] + What mode to run the program in [possible values: fast (alias: quick), slow] Options: -h, --help Print help information @@ -17,6 +17,12 @@ Hare $ 04_01_enum_derive slow Tortoise +$ 04_01_enum_derive quick +Hare + +$ 04_01_enum_derive zippy +Hare + $ 04_01_enum_derive medium ? failed error: "medium" isn't a valid value for '' diff --git a/examples/tutorial_derive/04_01_enum.rs b/examples/tutorial_derive/04_01_enum.rs index 078fc5d1398..32951f4ee23 100644 --- a/examples/tutorial_derive/04_01_enum.rs +++ b/examples/tutorial_derive/04_01_enum.rs @@ -10,6 +10,7 @@ struct Cli { #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] enum Mode { + #[value(visible_alias("quick"), alias("zippy"))] Fast, Slow, } diff --git a/src/builder/possible_value.rs b/src/builder/possible_value.rs index 03964fe118b..0a31d967ece 100644 --- a/src/builder/possible_value.rs +++ b/src/builder/possible_value.rs @@ -31,7 +31,7 @@ use crate::util::eq_ignore_case; pub struct PossibleValue { name: Str, help: Option, - aliases: Vec, // (name, visible) + aliases: Vec<(Str, bool)>, // (name, visible) hide: bool, } @@ -114,7 +114,7 @@ impl PossibleValue { #[must_use] pub fn alias(mut self, name: impl IntoResettable) -> Self { if let Some(name) = name.into_resettable().into_option() { - self.aliases.push(name); + self.aliases.push((name, false)); } else { self.aliases.clear(); } @@ -133,7 +133,45 @@ impl PossibleValue { /// ``` #[must_use] pub fn aliases(mut self, names: impl IntoIterator>) -> Self { - self.aliases.extend(names.into_iter().map(|a| a.into())); + self.aliases + .extend(names.into_iter().map(|a| (a.into(), false))); + self + } + + /// Sets a *visible* alias for this argument value. + /// + /// # Examples + /// + /// ```rust + /// # use clap::builder::PossibleValue; + /// PossibleValue::new("slow") + /// .visible_alias("not-fast") + /// # ; + /// ``` + #[must_use] + pub fn visible_alias(mut self, name: impl IntoResettable) -> Self { + if let Some(name) = name.into_resettable().into_option() { + self.aliases.push((name, true)); + } else { + self.aliases.clear(); + } + self + } + + /// Sets multiple *visible* aliases for this argument value. + /// + /// # Examples + /// + /// ```rust + /// # use clap::builder::PossibleValue; + /// PossibleValue::new("slow") + /// .visible_aliases(["not-fast", "snake-like"]) + /// # ; + /// ``` + #[must_use] + pub fn visible_aliases(mut self, names: impl IntoIterator>) -> Self { + self.aliases + .extend(names.into_iter().map(|a| (a.into(), true))); self } } @@ -180,21 +218,25 @@ impl PossibleValue { #[cfg(feature = "help")] pub(crate) fn get_visible_quoted_name(&self) -> Option> { if !self.hide { - Some(if self.name.contains(char::is_whitespace) { - format!("{:?}", self.name).into() - } else { - self.name.as_str().into() - }) + Some(quote_if_whitespace(&self.name)) } else { None } } + /// Get the names of all visible aliases, but wrapped in quotes if they contain whitespace + pub fn get_quoted_visible_aliases(&self) -> Vec> { + self.aliases + .iter() + .filter_map(|(n, vis)| vis.then(|| quote_if_whitespace(n))) + .collect() + } + /// Returns all valid values of the argument value. /// /// Namely the name and all aliases. pub fn get_name_and_aliases(&self) -> impl Iterator + '_ { - std::iter::once(self.get_name()).chain(self.aliases.iter().map(|s| s.as_str())) + std::iter::once(self.get_name()).chain(self.aliases.iter().map(|(s, _)| s.as_str())) } /// Tests if the value is valid for this argument value @@ -205,9 +247,10 @@ impl PossibleValue { /// /// ```rust /// # use clap::builder::PossibleValue; - /// let arg_value = PossibleValue::new("fast").alias("not-slow"); + /// let arg_value = PossibleValue::new("fast").visible_alias("quick").alias("not-slow"); /// /// assert!(arg_value.matches("fast", false)); + /// assert!(arg_value.matches("quick", false)); /// assert!(arg_value.matches("not-slow", false)); /// /// assert!(arg_value.matches("FAST", true)); @@ -223,6 +266,15 @@ impl PossibleValue { } } +/// Quote a string if it contains whitespace +fn quote_if_whitespace(n: &Str) -> std::borrow::Cow<'_, str> { + if n.contains(char::is_whitespace) { + format!("{:?}", n).into() + } else { + n.as_str().into() + } +} + impl> From for PossibleValue { fn from(s: S) -> Self { Self::new(s) diff --git a/src/output/help_template.rs b/src/output/help_template.rs index 078a9a9bd55..1906a1c5c16 100644 --- a/src/output/help_template.rs +++ b/src/output/help_template.rs @@ -599,6 +599,7 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { if let Some(arg) = arg { const DASH_SPACE: usize = "- ".len(); const COLON_SPACE: usize = ": ".len(); + const COMMA_SPACE: usize = ", ".len(); let possible_vals = arg.get_possible_values(); if self.use_long && !arg.is_hide_possible_values_set() @@ -620,7 +621,19 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { .expect("Only called with possible value"); let help_longest = possible_vals .iter() - .filter_map(|f| f.get_visible_help().map(|h| h.display_width())) + .map(|f| { + let help_width = + f.get_visible_help().map(|h| h.display_width()).unwrap_or(0); + let aliases = f.get_quoted_visible_aliases(); + let alias_width: usize = aliases.iter().map(|a| display_width(a)).sum(); + let comma_width = aliases.len().saturating_sub(1) * COMMA_SPACE; + let wrapper_width = if aliases.is_empty() { + 0 + } else { + " (alias: )".len() + }; + help_width + alias_width + comma_width + wrapper_width + }) .max() .expect("Only called with possible value with help"); // should new line @@ -642,7 +655,19 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { self.spaces(spaces); self.none("- "); self.literal(pv.get_name()); - if let Some(help) = pv.get_help() { + let mut opt_help = pv.get_help().cloned(); + let aliases = pv.get_quoted_visible_aliases().join(", "); + if !aliases.is_empty() { + let mut help = opt_help.unwrap_or_default(); + if !help.is_empty() { + help.none(" "); + } + help.none("(alias: "); + help.none(aliases); + help.none(")"); + opt_help = Some(help); + } + if let Some(mut help) = opt_help { debug!("HelpTemplate::help: Possible Value help"); if possible_value_new_line { @@ -660,7 +685,6 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { usize::MAX }; - let mut help = help.clone(); replace_newline_var(&mut help); help.wrap(avail_chars); help.indent("", &trailing_indent); @@ -783,7 +807,16 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { let pvs = possible_vals .iter() - .filter_map(PossibleValue::get_visible_quoted_name) + .filter_map(|pv| { + pv.get_visible_quoted_name().map(|n| { + let aliases = pv.get_quoted_visible_aliases().join(", "); + if aliases.is_empty() { + n + } else { + format!("{n} (alias: {aliases})").into() + } + }) + }) .collect::>() .join(", "); diff --git a/tests/derive/help.rs b/tests/derive/help.rs index 5cdcb9a3278..3541723a6b6 100644 --- a/tests/derive/help.rs +++ b/tests/derive/help.rs @@ -395,7 +395,7 @@ Arguments: Argument help Possible values: - - foo: Foo help + - foo: Foo help (alias: foozle) - bar: Bar help Options: @@ -414,6 +414,7 @@ Options: #[derive(clap::ValueEnum, PartialEq, Debug, Clone)] enum ArgChoice { /// Foo help + #[value(visible_alias("foozle"), alias("floofy"))] Foo, /// Bar help Bar,