From 200f6626dbc897695f65ec0ae3e4213b664adfb9 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 12 May 2022 14:17:35 -0500 Subject: [PATCH] feat(parser): Add type information to arg values To set the type, we offer - `ValueParser::` short cuts for natively supported types - `TypedValueParser` for fn pointers and custom implementations - `value_parser!(T)` for specialized lookup of an implementation (inspired by #2298) The main motivation for `value_parser!` is to help with `clap_derive`s implementation but it can also be convinient for end-users. When reading, this replaces nearly all of our current `ArgMatches` getters with: - `get_one`: like `value_of_t` - `get_many`: like `values_of_t` It also adds a `get_raw` that allows accessing the `OsStr`s without panicing. The naming is to invoke the idea of a more general container which I want to move this to. The return type is a bit complicated so that - Users choose whether to panic on invalid types so they can do their own querying, like `get_raw` - Users can choose how to handle not present easily (#2505) We had to defer setting the `value_parser` on external subcommands, for consistency sake, because `Command` requires `PartialEq` and `ValueParser` does not impl that trait. It'll have to wait until a breaking change. Fixes #2505 --- src/builder/arg.rs | 37 +++- src/builder/command.rs | 12 +- src/builder/mod.rs | 7 +- src/builder/value_parser.rs | 312 +++++++++++++++++++++++++++++- src/parser/matches/arg_matches.rs | 162 ++++++++++++++++ 5 files changed, 526 insertions(+), 4 deletions(-) diff --git a/src/builder/arg.rs b/src/builder/arg.rs index 437a03efe02..82b883e3df4 100644 --- a/src/builder/arg.rs +++ b/src/builder/arg.rs @@ -1001,6 +1001,27 @@ impl<'help> Arg<'help> { } } + /// Specify the type of the argument. + /// + /// This allows parsing and validating a value before storing it into + /// [`ArgMatches`][crate::ArgMatches]. + /// + /// ```rust + /// let cmd = clap::Command::new("raw") + /// .arg( + /// clap::Arg::new("port") + /// .value_parser(clap::value_parser!(usize)) + /// ); + /// let value_parser = cmd.get_arguments() + /// .find(|a| a.get_id() == "port").unwrap() + /// .get_value_parser(); + /// println!("{:?}", value_parser); + /// ``` + pub fn value_parser(mut self, parser: impl Into) -> Self { + self.value_parser = Some(parser.into()); + self + } + /// Specifies that the argument may have an unknown number of values /// /// Without any other settings, this argument may appear only *once*. @@ -4739,7 +4760,21 @@ impl<'help> Arg<'help> { } /// Configured parser for argument values - pub(crate) fn get_value_parser(&self) -> &super::ValueParser { + /// + /// # Example + /// + /// ```rust + /// let cmd = clap::Command::new("raw") + /// .arg( + /// clap::Arg::new("port") + /// .value_parser(clap::value_parser!(usize)) + /// ); + /// let value_parser = cmd.get_arguments() + /// .find(|a| a.get_id() == "port").unwrap() + /// .get_value_parser(); + /// println!("{:?}", value_parser); + /// ``` + pub fn get_value_parser(&self) -> &super::ValueParser { if let Some(value_parser) = self.value_parser.as_ref() { value_parser } else if self.is_allow_invalid_utf8_set() { diff --git a/src/builder/command.rs b/src/builder/command.rs index 23b2ee930af..36b160aff15 100644 --- a/src/builder/command.rs +++ b/src/builder/command.rs @@ -3679,7 +3679,17 @@ impl<'help> App<'help> { } /// Configured parser for values passed to an external subcommand - pub(crate) fn get_external_subcommand_value_parser(&self) -> Option<&super::ValueParser> { + /// + /// # Example + /// + /// ```rust + /// let cmd = clap::Command::new("raw") + /// .allow_external_subcommands(true) + /// .allow_invalid_utf8_for_external_subcommands(true); + /// let value_parser = cmd.get_external_subcommand_value_parser(); + /// println!("{:?}", value_parser); + /// ``` + pub fn get_external_subcommand_value_parser(&self) -> Option<&super::ValueParser> { if !self.is_allow_external_subcommands_set() { None } else if self.is_allow_invalid_utf8_for_external_subcommands_set() { diff --git a/src/builder/mod.rs b/src/builder/mod.rs index 2394ffcfae2..5c982e1c3db 100644 --- a/src/builder/mod.rs +++ b/src/builder/mod.rs @@ -30,7 +30,12 @@ pub use arg_settings::{ArgFlags, ArgSettings}; pub use command::Command; pub use possible_value::PossibleValue; pub use value_hint::ValueHint; -pub(crate) use value_parser::ValueParser; +pub use value_parser::AnyValueParser; +pub use value_parser::AutoValueParser; +pub use value_parser::TypedValueParser; +pub use value_parser::ValueParser; +pub use value_parser::ValueParserViaBuiltIn; +pub use value_parser::ValueParserViaFromStr; #[allow(deprecated)] pub use command::App; diff --git a/src/builder/value_parser.rs b/src/builder/value_parser.rs index 8b3894203eb..36384769f4f 100644 --- a/src/builder/value_parser.rs +++ b/src/builder/value_parser.rs @@ -1,3 +1,4 @@ +use std::any::TypeId; use std::sync::Arc; use crate::parser::AnyValue; @@ -10,9 +11,16 @@ pub struct ValueParser(pub(crate) ValueParserInner); pub(crate) enum ValueParserInner { String, OsString, + PathBuf, + Other(Arc), } impl ValueParser { + /// Custom parser for argument values + pub fn new(other: impl AnyValueParser + Send + Sync + 'static) -> Self { + Self(ValueParserInner::Other(Arc::new(other))) + } + /// `String` parser for argument values pub const fn string() -> Self { Self(ValueParserInner::String) @@ -22,6 +30,11 @@ impl ValueParser { pub const fn os_string() -> Self { Self(ValueParserInner::OsString) } + + /// `PathBuf` parser for argument values + pub const fn path_buf() -> Self { + Self(ValueParserInner::PathBuf) + } } impl ValueParser { @@ -31,7 +44,7 @@ impl ValueParser { pub fn parse_ref( &self, cmd: &crate::Command, - _arg: Option<&crate::Arg>, + arg: Option<&crate::Arg>, value: &std::ffi::OsStr, ) -> Result { match &self.0 { @@ -45,15 +58,312 @@ impl ValueParser { Ok(Arc::new(value.to_owned())) } ValueParserInner::OsString => Ok(Arc::new(value.to_owned())), + ValueParserInner::PathBuf => Ok(Arc::new(std::path::PathBuf::from(value))), + ValueParserInner::Other(o) => o.parse_ref(cmd, arg, value), + } + } + + /// Parse into a `Arc` + /// + /// When `arg` is `None`, an external subcommand value is being parsed. + pub fn parse( + &self, + cmd: &crate::Command, + arg: Option<&crate::Arg>, + value: std::ffi::OsString, + ) -> Result { + match &self.0 { + ValueParserInner::String => { + let value = value.into_string().map_err(|_| { + crate::Error::invalid_utf8( + cmd, + crate::output::Usage::new(cmd).create_usage_with_title(&[]), + ) + })?; + Ok(Arc::new(value)) + } + ValueParserInner::OsString => Ok(Arc::new(value)), + ValueParserInner::PathBuf => Ok(Arc::new(std::path::PathBuf::from(value))), + ValueParserInner::Other(o) => o.parse(cmd, arg, value), + } + } + + /// Describes the content of `Arc` + pub fn type_id(&self) -> TypeId { + match &self.0 { + ValueParserInner::String => TypeId::of::(), + ValueParserInner::OsString => TypeId::of::(), + ValueParserInner::PathBuf => TypeId::of::(), + ValueParserInner::Other(o) => o.type_id(), + } + } + + /// Describes the content of `Arc` + pub fn type_name(&self) -> &'static str { + match &self.0 { + ValueParserInner::String => std::any::type_name::(), + ValueParserInner::OsString => std::any::type_name::(), + ValueParserInner::PathBuf => std::any::type_name::(), + ValueParserInner::Other(o) => o.type_name(), } } } +impl From

for ValueParser { + fn from(p: P) -> Self { + ValueParser(ValueParserInner::Other(Arc::new(p))) + } +} + impl<'help> std::fmt::Debug for ValueParser { fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { match &self.0 { ValueParserInner::String => f.debug_struct("ValueParser::string").finish(), ValueParserInner::OsString => f.debug_struct("ValueParser::os_string").finish(), + ValueParserInner::PathBuf => f.debug_struct("ValueParser::path_buf").finish(), + ValueParserInner::Other(o) => write!(f, "ValueParser::other({})", o.type_name()), } } } + +// Require people to implement `TypedValueParser` rather than `AnyValueParser`: +// - Make implementing the user-facing trait easier +// - Enforce in the type-system that a given `AnyValueParser::parse` always returns the same type +// on each call and that it matches `type_id` / `type_name` +/// Parse/validate argument values into a `Arc` +pub trait AnyValueParser: private::AnyValueParserSealed { + /// Parse into a `Arc` + /// + /// When `arg` is `None`, an external subcommand value is being parsed. + fn parse_ref( + &self, + cmd: &crate::Command, + arg: Option<&crate::Arg>, + value: &std::ffi::OsStr, + ) -> Result; + + /// Parse into a `Arc` + /// + /// When `arg` is `None`, an external subcommand value is being parsed. + fn parse( + &self, + cmd: &crate::Command, + arg: Option<&crate::Arg>, + value: std::ffi::OsString, + ) -> Result; + + /// Describes the content of `Arc` + fn type_id(&self) -> TypeId; + + /// Describes the content of `Arc` + fn type_name(&self) -> &'static str; +} + +impl AnyValueParser for P +where + T: std::any::Any + Send + Sync + 'static, + P: TypedValueParser, +{ + fn parse_ref( + &self, + cmd: &crate::Command, + arg: Option<&crate::Arg>, + value: &std::ffi::OsStr, + ) -> Result { + let value = TypedValueParser::parse_ref(self, cmd, arg, value)?; + Ok(Arc::new(value)) + } + + fn parse( + &self, + cmd: &crate::Command, + arg: Option<&crate::Arg>, + value: std::ffi::OsString, + ) -> Result { + let value = TypedValueParser::parse(self, cmd, arg, value)?; + Ok(Arc::new(value)) + } + + fn type_id(&self) -> TypeId { + TypeId::of::() + } + + fn type_name(&self) -> &'static str { + std::any::type_name::() + } +} + +/// Parse/validate argument values +pub trait TypedValueParser { + /// Argument's value type + type Value; + + /// Parse the argument value + /// + /// When `arg` is `None`, an external subcommand value is being parsed. + fn parse_ref( + &self, + cmd: &crate::Command, + arg: Option<&crate::Arg>, + value: &std::ffi::OsStr, + ) -> Result; + + /// Parse the argument value + /// + /// When `arg` is `None`, an external subcommand value is being parsed. + fn parse( + &self, + cmd: &crate::Command, + arg: Option<&crate::Arg>, + value: std::ffi::OsString, + ) -> Result { + self.parse_ref(cmd, arg, &value) + } +} + +impl TypedValueParser for fn(&str) -> Result +where + E: Into>, +{ + type Value = T; + + fn parse_ref( + &self, + cmd: &crate::Command, + arg: Option<&crate::Arg>, + value: &std::ffi::OsStr, + ) -> Result { + let value = value.to_str().ok_or_else(|| { + crate::Error::invalid_utf8( + cmd, + crate::output::Usage::new(cmd).create_usage_with_title(&[]), + ) + })?; + let value = (self)(value).map_err(|e| { + let arg = arg + .map(|a| a.to_string()) + .unwrap_or_else(|| "...".to_owned()); + crate::Error::value_validation(arg, value.to_owned(), e.into()).with_cmd(cmd) + })?; + Ok(value) + } +} + +impl TypedValueParser for fn(&std::ffi::OsStr) -> Result +where + E: Into>, +{ + type Value = T; + + fn parse_ref( + &self, + cmd: &crate::Command, + arg: Option<&crate::Arg>, + value: &std::ffi::OsStr, + ) -> Result { + let value = (self)(value).map_err(|e| { + let arg = arg + .map(|a| a.to_string()) + .unwrap_or_else(|| "...".to_owned()); + crate::Error::value_validation(arg, value.to_string_lossy().into_owned(), e.into()) + .with_cmd(cmd) + })?; + Ok(value) + } +} + +#[doc(hidden)] +#[derive(Debug)] +pub struct AutoValueParser(std::marker::PhantomData); + +impl AutoValueParser { + #[doc(hidden)] + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self(Default::default()) + } +} + +#[doc(hidden)] +pub trait ValueParserViaBuiltIn: private::ValueParserViaBuiltInSealed { + fn value_parser(&self) -> ValueParser; +} +impl ValueParserViaBuiltIn for &AutoValueParser { + fn value_parser(&self) -> ValueParser { + ValueParser::string() + } +} +impl ValueParserViaBuiltIn for &AutoValueParser { + fn value_parser(&self) -> ValueParser { + ValueParser::os_string() + } +} +impl ValueParserViaBuiltIn for &AutoValueParser { + fn value_parser(&self) -> ValueParser { + ValueParser::path_buf() + } +} + +#[doc(hidden)] +pub trait ValueParserViaFromStr: private::ValueParserViaFromStrSealed { + fn value_parser(&self) -> ValueParser; +} +impl ValueParserViaFromStr for AutoValueParser +where + FromStr: std::str::FromStr + std::any::Any + Send + Sync + 'static, + ::Err: Into>, +{ + fn value_parser(&self) -> ValueParser { + let func: fn(&str) -> Result::Err> = + FromStr::from_str; + ValueParser::new(func) + } +} + +/// Parse/validate argument values +/// +/// # Example +/// +/// ```rust +/// let parser = clap::value_parser!(String); +/// assert_eq!(format!("{:?}", parser), "ValueParser::string"); +/// let parser = clap::value_parser!(std::ffi::OsString); +/// assert_eq!(format!("{:?}", parser), "ValueParser::os_string"); +/// let parser = clap::value_parser!(std::path::PathBuf); +/// assert_eq!(format!("{:?}", parser), "ValueParser::path_buf"); +/// let parser = clap::value_parser!(usize); +/// assert_eq!(format!("{:?}", parser), "ValueParser::other(usize)"); +/// ``` +#[macro_export] +macro_rules! value_parser { + ($name:ty) => {{ + use $crate::builder::ValueParserViaBuiltIn; + use $crate::builder::ValueParserViaFromStr; + let auto = $crate::builder::AutoValueParser::<$name>::new(); + (&&auto).value_parser() + }}; +} + +mod private { + pub trait AnyValueParserSealed {} + impl AnyValueParserSealed for P + where + T: std::any::Any + Send + Sync + 'static, + P: super::TypedValueParser, + { + } + + pub trait ValueParserViaBuiltInSealed {} + impl ValueParserViaBuiltInSealed for &super::AutoValueParser {} + impl ValueParserViaBuiltInSealed for &super::AutoValueParser {} + impl ValueParserViaBuiltInSealed for &super::AutoValueParser {} + + pub trait ValueParserViaFromStrSealed {} + impl ValueParserViaFromStrSealed for super::AutoValueParser + where + FromStr: std::str::FromStr + std::any::Any + Send + Sync + 'static, + ::Err: + Into>, + { + } +} diff --git a/src/parser/matches/arg_matches.rs b/src/parser/matches/arg_matches.rs index 7b8b3a37579..c8c3393482d 100644 --- a/src/parser/matches/arg_matches.rs +++ b/src/parser/matches/arg_matches.rs @@ -78,6 +78,168 @@ pub struct ArgMatches { } impl ArgMatches { + /// Gets the value of a specific option or positional argument. + /// + /// i.e. an argument that [takes an additional value][crate::Arg::takes_value] at runtime. + /// + /// Returns an error if the wrong type was used. + /// + /// Returns `None` if the option wasn't present. + /// + /// *NOTE:* This will always return `Some(value)` if [`default_value`] has been set. + /// [`occurrences_of`] can be used to check if a value is present at runtime. + /// + /// # Panics + /// If `id` is is not a valid argument or group name. + /// + /// # Examples + /// + /// ```rust + /// # use clap::{Command, Arg, value_parser}; + /// let m = Command::new("myapp") + /// .arg(Arg::new("port") + /// .value_parser(value_parser!(usize)) + /// .takes_value(true) + /// .required(true)) + /// .get_matches_from(vec!["myapp", "2020"]); + /// + /// let port: usize = *m + /// .get_one("port") + /// .expect("`port` is a `usize`") + /// .expect("`port`is required"); + /// assert_eq!(port, 2020); + /// ``` + /// [option]: crate::Arg::takes_value() + /// [positional]: crate::Arg::index() + /// [`ArgMatches::values_of`]: ArgMatches::values_of() + /// [`default_value`]: crate::Arg::default_value() + /// [`occurrences_of`]: crate::ArgMatches::occurrences_of() + pub fn get_one(&self, name: &str) -> Result, Error> { + let id = Id::from(name); + let value = match self.get_arg(&id).and_then(|a| a.first()) { + Some(value) => value, + None => { + return Ok(None); + } + }; + value.downcast_ref::().map(Some).ok_or_else(|| { + Error::raw( + crate::error::ErrorKind::ValueValidation, + format!( + "The argument `{}` is not of type `{}`", + name, + std::any::type_name::() + ), + ) + }) + } + + /// Iterate over [values] of a specific option or positional argument. + /// + /// i.e. an argument that takes multiple values at runtime. + /// + /// Returns an error if the wrong type was used. + /// + /// Returns `None` if the option wasn't present. + /// + /// # Panics + /// + /// If `id` is is not a valid argument or group name. + /// + /// # Examples + /// + /// ```rust + /// # use clap::{Command, Arg, value_parser}; + /// let m = Command::new("myprog") + /// .arg(Arg::new("ports") + /// .multiple_occurrences(true) + /// .value_parser(value_parser!(usize)) + /// .short('p') + /// .takes_value(true)) + /// .get_matches_from(vec![ + /// "myprog", "-p", "22", "-p", "80", "-p", "2020" + /// ]); + /// let vals: Vec = m.get_many("ports") + /// .expect("`port` is a `usize`") + /// .expect("`port`is required") + /// .copied() + /// .collect(); + /// assert_eq!(vals, [22, 80, 2020]); + /// ``` + /// [values]: Values + pub fn get_many( + &self, + name: &str, + ) -> Result>, Error> { + let id = Id::from(name); + let values = match self.get_arg(&id) { + Some(values) => values.vals_flatten(), + None => { + return Ok(None); + } + }; + // HACK: Track the type id and report errors even when there are no values + let values: Result, Error> = values + .map(|v| { + v.downcast_ref::().ok_or_else(|| { + Error::raw( + crate::error::ErrorKind::ValueValidation, + format!( + "The argument `{}` is not of type `{}`", + name, + std::any::type_name::() + ), + ) + }) + }) + .collect(); + Ok(Some(values?.into_iter())) + } + + /// Iterate over the original argument values. + /// + /// An `OsStr` on Unix-like systems is any series of bytes, regardless of whether or not they + /// contain valid UTF-8. Since [`String`]s in Rust are guaranteed to be valid UTF-8, a valid + /// filename on a Unix system as an argument value may contain invalid UTF-8. + /// + /// Returns `None` if the option wasn't present. + /// + /// # Panics + /// + /// If `id` is is not a valid argument or group name. + /// + /// # Examples + /// + #[cfg_attr(not(unix), doc = " ```ignore")] + #[cfg_attr(unix, doc = " ```")] + /// # use clap::{Command, arg, value_parser}; + /// # use std::ffi::{OsStr,OsString}; + /// # use std::os::unix::ffi::{OsStrExt,OsStringExt}; + /// use std::path::PathBuf; + /// + /// let m = Command::new("utf8") + /// .arg(arg!( ... "some arg").value_parser(value_parser!(PathBuf))) + /// .get_matches_from(vec![OsString::from("myprog"), + /// // "Hi" + /// OsString::from_vec(vec![b'H', b'i']), + /// // "{0xe9}!" + /// OsString::from_vec(vec![0xe9, b'!'])]); + /// + /// let mut itr = m.get_raw("arg").unwrap().into_iter(); + /// assert_eq!(itr.next(), Some(OsStr::new("Hi"))); + /// assert_eq!(itr.next(), Some(OsStr::from_bytes(&[0xe9, b'!']))); + /// assert_eq!(itr.next(), None); + /// ``` + /// [`Iterator`]: std::iter::Iterator + /// [`OsSt`]: std::ffi::OsStr + /// [values]: OsValues + /// [`String`]: std::string::String + pub fn get_raw(&self, id: T) -> Option> { + let id = Id::from(id); + let arg = self.get_arg(&id)?; + Some(arg.raw_vals_flatten().map(|v| v.as_os_str())) + } + /// Check if any args were present on the command line /// /// # Examples