Skip to content

Commit

Permalink
feat(parser): Make customizeing flags easier
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
epage authored and Calder-Ty committed Aug 24, 2022
1 parent fa20acf commit fa09fd7
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 6 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 35 additions & 0 deletions src/builder/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
///
Expand Down
1 change: 1 addition & 0 deletions src/builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
103 changes: 103 additions & 0 deletions src/builder/value_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,50 @@ 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
Expand Down Expand Up @@ -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<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
Expand Down
16 changes: 10 additions & 6 deletions tests/derive/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
}

Expand Down

0 comments on commit fa09fd7

Please sign in to comment.