feat(parser): Add type information to arg values

To set the type, we offer
- `ValueParser::<type>` 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
This commit is contained in:
Ed Page 2022-05-12 14:17:35 -05:00
parent dcf69d1c87
commit 200f6626db
5 changed files with 526 additions and 4 deletions

View file

@ -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<super::ValueParser>) -> 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() {

View file

@ -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() {

View file

@ -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;

View file

@ -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<dyn AnyValueParser + Send + Sync + 'static>),
}
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<AnyValue, crate::Error> {
match &self.0 {
@ -45,8 +58,61 @@ 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<Any>`
///
/// 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<AnyValue, crate::Error> {
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<Any>`
pub fn type_id(&self) -> TypeId {
match &self.0 {
ValueParserInner::String => TypeId::of::<String>(),
ValueParserInner::OsString => TypeId::of::<std::ffi::OsString>(),
ValueParserInner::PathBuf => TypeId::of::<std::path::PathBuf>(),
ValueParserInner::Other(o) => o.type_id(),
}
}
/// Describes the content of `Arc<Any>`
pub fn type_name(&self) -> &'static str {
match &self.0 {
ValueParserInner::String => std::any::type_name::<String>(),
ValueParserInner::OsString => std::any::type_name::<std::ffi::OsString>(),
ValueParserInner::PathBuf => std::any::type_name::<std::path::PathBuf>(),
ValueParserInner::Other(o) => o.type_name(),
}
}
}
impl<P: AnyValueParser + Send + Sync + 'static> From<P> for ValueParser {
fn from(p: P) -> Self {
ValueParser(ValueParserInner::Other(Arc::new(p)))
}
}
impl<'help> std::fmt::Debug for ValueParser {
@ -54,6 +120,250 @@ impl<'help> std::fmt::Debug for ValueParser {
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<Any>`
pub trait AnyValueParser: private::AnyValueParserSealed {
/// Parse into a `Arc<Any>`
///
/// 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<AnyValue, crate::Error>;
/// Parse into a `Arc<Any>`
///
/// 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<AnyValue, crate::Error>;
/// Describes the content of `Arc<Any>`
fn type_id(&self) -> TypeId;
/// Describes the content of `Arc<Any>`
fn type_name(&self) -> &'static str;
}
impl<T, P> AnyValueParser for P
where
T: std::any::Any + Send + Sync + 'static,
P: TypedValueParser<Value = T>,
{
fn parse_ref(
&self,
cmd: &crate::Command,
arg: Option<&crate::Arg>,
value: &std::ffi::OsStr,
) -> Result<AnyValue, crate::Error> {
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<AnyValue, crate::Error> {
let value = TypedValueParser::parse(self, cmd, arg, value)?;
Ok(Arc::new(value))
}
fn type_id(&self) -> TypeId {
TypeId::of::<T>()
}
fn type_name(&self) -> &'static str {
std::any::type_name::<T>()
}
}
/// 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<Self::Value, crate::Error>;
/// 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::Value, crate::Error> {
self.parse_ref(cmd, arg, &value)
}
}
impl<T, E> TypedValueParser for fn(&str) -> Result<T, E>
where
E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
{
type Value = T;
fn parse_ref(
&self,
cmd: &crate::Command,
arg: Option<&crate::Arg>,
value: &std::ffi::OsStr,
) -> Result<T, crate::Error> {
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<T, E> TypedValueParser for fn(&std::ffi::OsStr) -> Result<T, E>
where
E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
{
type Value = T;
fn parse_ref(
&self,
cmd: &crate::Command,
arg: Option<&crate::Arg>,
value: &std::ffi::OsStr,
) -> Result<T, crate::Error> {
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<T>(std::marker::PhantomData<T>);
impl<T> AutoValueParser<T> {
#[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<String> {
fn value_parser(&self) -> ValueParser {
ValueParser::string()
}
}
impl ValueParserViaBuiltIn for &AutoValueParser<std::ffi::OsString> {
fn value_parser(&self) -> ValueParser {
ValueParser::os_string()
}
}
impl ValueParserViaBuiltIn for &AutoValueParser<std::path::PathBuf> {
fn value_parser(&self) -> ValueParser {
ValueParser::path_buf()
}
}
#[doc(hidden)]
pub trait ValueParserViaFromStr: private::ValueParserViaFromStrSealed {
fn value_parser(&self) -> ValueParser;
}
impl<FromStr> ValueParserViaFromStr for AutoValueParser<FromStr>
where
FromStr: std::str::FromStr + std::any::Any + Send + Sync + 'static,
<FromStr as std::str::FromStr>::Err: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
{
fn value_parser(&self) -> ValueParser {
let func: fn(&str) -> Result<FromStr, <FromStr as std::str::FromStr>::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<T, P> AnyValueParserSealed for P
where
T: std::any::Any + Send + Sync + 'static,
P: super::TypedValueParser<Value = T>,
{
}
pub trait ValueParserViaBuiltInSealed {}
impl ValueParserViaBuiltInSealed for &super::AutoValueParser<String> {}
impl ValueParserViaBuiltInSealed for &super::AutoValueParser<std::ffi::OsString> {}
impl ValueParserViaBuiltInSealed for &super::AutoValueParser<std::path::PathBuf> {}
pub trait ValueParserViaFromStrSealed {}
impl<FromStr> ValueParserViaFromStrSealed for super::AutoValueParser<FromStr>
where
FromStr: std::str::FromStr + std::any::Any + Send + Sync + 'static,
<FromStr as std::str::FromStr>::Err:
Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
{
}
}

View file

@ -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<T: 'static>(&self, name: &str) -> Result<Option<&T>, 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::<T>().map(Some).ok_or_else(|| {
Error::raw(
crate::error::ErrorKind::ValueValidation,
format!(
"The argument `{}` is not of type `{}`",
name,
std::any::type_name::<T>()
),
)
})
}
/// 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<usize> = 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<T: 'static>(
&self,
name: &str,
) -> Result<Option<impl Iterator<Item = &T>>, 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<Vec<&T>, Error> = values
.map(|v| {
v.downcast_ref::<T>().ok_or_else(|| {
Error::raw(
crate::error::ErrorKind::ValueValidation,
format!(
"The argument `{}` is not of type `{}`",
name,
std::any::type_name::<T>()
),
)
})
})
.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!(<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<T: Key>(&self, id: T) -> Option<impl Iterator<Item = &OsStr>> {
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