diff --git a/CHANGELOG.md b/CHANGELOG.md index 45692f548b8f..bd1d6982f94f 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 1de512a4cefe..56eb446653c0 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::("flag").copied(), + /// Some(10) + /// ); + /// + /// let matches = cmd.try_get_matches_from(["mycmd"]).unwrap(); + /// assert!(matches.contains_id("flag")); + /// assert_eq!( + /// matches.get_one::("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 a147f1eb2597..16eaeffd2ec3 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 7262f9c7d92f..769d1a70a993 100644 --- a/src/builder/value_parser.rs +++ b/src/builder/value_parser.rs @@ -635,6 +635,50 @@ pub trait TypedValueParser: Clone + Send + Sync + 'static { ) -> Option> + '_>> { 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::("flag").copied(), + /// Some(10) + /// ); + /// + /// let matches = cmd.try_get_matches_from(["mycmd"]).unwrap(); + /// assert!(matches.contains_id("flag")); + /// assert_eq!( + /// matches.get_one::("flag").copied(), + /// Some(5) + /// ); + /// ``` + fn map(self, func: F) -> MapValueParser + where + T: Send + Sync + Clone, + F: Fn(Self::Value) -> T + Clone, + { + MapValueParser::new(self, func) + } } impl TypedValueParser for F @@ -1776,6 +1820,65 @@ impl Default for NonEmptyStringValueParser { } } +/// Adapt a `TypedValueParser` from one value to another +/// +/// See [`TypedValueParser::map`] +#[derive(Clone, Debug)] +pub struct MapValueParser { + parser: P, + func: F, +} + +impl MapValueParser +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 TypedValueParser for MapValueParser +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 { + 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 { + let value = self.parser.parse(cmd, arg, value)?; + let value = (self.func)(value); + Ok(value) + } + + fn possible_values( + &self, + ) -> Option> + '_>> { + self.parser.possible_values() + } +} + /// Register a type with [value_parser!][crate::value_parser!] /// /// # Example diff --git a/tests/derive/flags.rs b/tests/derive/flags.rs index 5a7920817321..dd15823b6fb3 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 { - b.parse::() - .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, }