Repository: clap-rs/clap
Pull request #4095 (2022-08-19 13:22:21)

feat(parser): Make customizeing flags easier
While `TypedValueParser` will generally make it easier to reuse value
parsers, this was particularly written for flags.  Besides having a
concrete API to document, an advantage over `fn(&str) -> Result<bool, E>`
value parsers is you get all of the benefits of the existing value
parsers for environment variable parsing.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 45692f54..bd1d6982 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -58,6 +58,7 @@ MSRV is now 1.60.0
 ### Features
 
 - `Arg::num_args` now accepts ranges, allowing setting both the minimum and maximum number of values per occurrence
+- Added `TypedValueParser::map` to make it easier to reuse existing value parsers
 - *(help)* Show `PossibleValue::help` in long help (`--help`) (#3312)
 
 ### Fixes
diff --git a/src/builder/action.rs b/src/builder/action.rs
index 1de512a4..56eb4466 100644
--- a/src/builder/action.rs
+++ b/src/builder/action.rs
@@ -101,6 +101,41 @@ pub enum ArgAction {
     ///     Some(false)
     /// );
     /// ```
+    ///
+    /// You can use [`TypedValueParser::map`][crate::builder::TypedValueParser::map] to have the
+    /// flag control an application-specific type:
+    /// ```rust
+    /// # use clap::Command;
+    /// # use clap::Arg;
+    /// # use clap::builder::TypedValueParser as _;
+    /// # use clap::builder::BoolishValueParser;
+    /// let cmd = Command::new("mycmd")
+    ///     .arg(
+    ///         Arg::new("flag")
+    ///             .long("flag")
+    ///             .action(clap::ArgAction::SetTrue)
+    ///             .value_parser(
+    ///                 BoolishValueParser::new()
+    ///                 .map(|b| -> usize {
+    ///                     if b { 10 } else { 5 }
+    ///                 })
+    ///             )
+    ///     );
+    ///
+    /// let matches = cmd.clone().try_get_matches_from(["mycmd", "--flag", "--flag"]).unwrap();
+    /// assert!(matches.contains_id("flag"));
+    /// assert_eq!(
+    ///     matches.get_one::<usize>("flag").copied(),
+    ///     Some(10)
+    /// );
+    ///
+    /// let matches = cmd.try_get_matches_from(["mycmd"]).unwrap();
+    /// assert!(matches.contains_id("flag"));
+    /// assert_eq!(
+    ///     matches.get_one::<usize>("flag").copied(),
+    ///     Some(5)
+    /// );
+    /// ```
     SetTrue,
     /// When encountered, act as if `"false"` was encountered on the command-line
     ///
diff --git a/src/builder/mod.rs b/src/builder/mod.rs
index a147f1eb..16eaeffd 100644
--- a/src/builder/mod.rs
+++ b/src/builder/mod.rs
@@ -32,6 +32,7 @@ pub use value_parser::BoolValueParser;
 pub use value_parser::BoolishValueParser;
 pub use value_parser::EnumValueParser;
 pub use value_parser::FalseyValueParser;
+pub use value_parser::MapValueParser;
 pub use value_parser::NonEmptyStringValueParser;
 pub use value_parser::OsStringValueParser;
 pub use value_parser::PathBufValueParser;
diff --git a/src/builder/value_parser.rs b/src/builder/value_parser.rs
index 322b68d6..769d1a70 100644
--- a/src/builder/value_parser.rs
+++ b/src/builder/value_parser.rs
@@ -108,7 +108,6 @@ impl ValueParser {
     pub fn new<P>(other: P) -> Self
     where
         P: TypedValueParser,
-        P::Value: Send + Sync + Clone,
     {
         Self(ValueParserInner::Other(Box::new(other)))
     }
@@ -284,7 +283,6 @@ impl ValueParser {
 impl<P> From<P> for ValueParser
 where
     P: TypedValueParser + Send + Sync + 'static,
-    P::Value: Send + Sync + Clone,
 {
     fn from(p: P) -> Self {
         Self::new(p)
@@ -604,7 +602,7 @@ where
 /// Parse/validate argument values
 pub trait TypedValueParser: Clone + Send + Sync + 'static {
     /// Argument's value type
-    type Value;
+    type Value: Send + Sync + Clone;
 
     /// Parse the argument value
     ///
@@ -637,12 +635,57 @@ pub trait TypedValueParser: Clone + Send + Sync + 'static {
     ) -> Option<Box<dyn Iterator<Item = crate::builder::PossibleValue<'static>> + '_>> {
         None
     }
+
+    /// Adapt a `TypedValueParser` from one value to another
+    ///
+    /// # Example
+    ///
+    /// ```rust
+    /// # use clap::Command;
+    /// # use clap::Arg;
+    /// # use clap::builder::TypedValueParser as _;
+    /// # use clap::builder::BoolishValueParser;
+    /// let cmd = Command::new("mycmd")
+    ///     .arg(
+    ///         Arg::new("flag")
+    ///             .long("flag")
+    ///             .action(clap::ArgAction::SetTrue)
+    ///             .value_parser(
+    ///                 BoolishValueParser::new()
+    ///                 .map(|b| -> usize {
+    ///                     if b { 10 } else { 5 }
+    ///                 })
+    ///             )
+    ///     );
+    ///
+    /// let matches = cmd.clone().try_get_matches_from(["mycmd", "--flag", "--flag"]).unwrap();
+    /// assert!(matches.contains_id("flag"));
+    /// assert_eq!(
+    ///     matches.get_one::<usize>("flag").copied(),
+    ///     Some(10)
+    /// );
+    ///
+    /// let matches = cmd.try_get_matches_from(["mycmd"]).unwrap();
+    /// assert!(matches.contains_id("flag"));
+    /// assert_eq!(
+    ///     matches.get_one::<usize>("flag").copied(),
+    ///     Some(5)
+    /// );
+    /// ```
+    fn map<T, F>(self, func: F) -> MapValueParser<Self, F>
+    where
+        T: Send + Sync + Clone,
+        F: Fn(Self::Value) -> T + Clone,
+    {
+        MapValueParser::new(self, func)
+    }
 }
 
 impl<F, T, E> TypedValueParser for F
 where
     F: Fn(&str) -> Result<T, E> + Clone + Send + Sync + 'static,
     E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
+    T: Send + Sync + Clone,
 {
     type Value = T;
 
@@ -1777,6 +1820,65 @@ impl Default for NonEmptyStringValueParser {
     }
 }
 
+/// Adapt a `TypedValueParser` from one value to another
+///
+/// See [`TypedValueParser::map`]
+#[derive(Clone, Debug)]
+pub struct MapValueParser<P, F> {
+    parser: P,
+    func: F,
+}
+
+impl<P, F, T> MapValueParser<P, F>
+where
+    P: TypedValueParser,
+    P::Value: Send + Sync + Clone,
+    F: Fn(P::Value) -> T + Clone,
+    T: Send + Sync + Clone,
+{
+    fn new(parser: P, func: F) -> Self {
+        Self { parser, func }
+    }
+}
+
+impl<P, F, T> TypedValueParser for MapValueParser<P, F>
+where
+    P: TypedValueParser,
+    P::Value: Send + Sync + Clone,
+    F: Fn(P::Value) -> T + Clone + Send + Sync + 'static,
+    T: Send + Sync + Clone,
+{
+    type Value = T;
+
+    fn parse_ref(
+        &self,
+        cmd: &crate::Command,
+        arg: Option<&crate::Arg>,
+        value: &std::ffi::OsStr,
+    ) -> Result<Self::Value, crate::Error> {
+        let value = self.parser.parse_ref(cmd, arg, value)?;
+        let value = (self.func)(value);
+        Ok(value)
+    }
+
+    fn parse(
+        &self,
+        cmd: &crate::Command,
+        arg: Option<&crate::Arg>,
+        value: std::ffi::OsString,
+    ) -> Result<Self::Value, crate::Error> {
+        let value = self.parser.parse(cmd, arg, value)?;
+        let value = (self.func)(value);
+        Ok(value)
+    }
+
+    fn possible_values(
+        &self,
+    ) -> Option<Box<dyn Iterator<Item = crate::builder::PossibleValue<'static>> + '_>> {
+        self.parser.possible_values()
+    }
+}
+
 /// Register a type with [value_parser!][crate::value_parser!]
 ///
 /// # Example


Test patch:
diff --git a/tests/derive/flags.rs b/tests/derive/flags.rs
index 5a792081..dd15823b 100644
--- a/tests/derive/flags.rs
+++ b/tests/derive/flags.rs
@@ -12,6 +12,8 @@
 // commit#ea76fa1b1b273e65e3b0b1046643715b49bec51f which is licensed under the
 // MIT/Apache 2.0 license.
 
+use clap::builder::BoolishValueParser;
+use clap::builder::TypedValueParser as _;
 use clap::ArgAction;
 use clap::CommandFactory;
 use clap::Parser;
@@ -46,17 +48,19 @@ fn bool_type_is_flag() {
 
 #[test]
 fn non_bool_type_flag() {
-    fn parse_from_flag(b: &str) -> Result<usize, String> {
-        b.parse::<bool>()
-            .map(|b| if b { 10 } else { 5 })
-            .map_err(|e| e.to_string())
+    fn parse_from_flag(b: bool) -> usize {
+        if b {
+            10
+        } else {
+            5
+        }
     }
 
     #[derive(Parser, Debug)]
     struct Opt {
-        #[clap(short, long, action = ArgAction::SetTrue, value_parser = parse_from_flag)]
+        #[clap(short, long, action = ArgAction::SetTrue, value_parser = BoolishValueParser::new().map(parse_from_flag))]
         alice: usize,
-        #[clap(short, long, action = ArgAction::SetTrue, value_parser = parse_from_flag)]
+        #[clap(short, long, action = ArgAction::SetTrue, value_parser = BoolishValueParser::new().map(parse_from_flag))]
         bob: usize,
     }