Merge pull request #2762 from ModProg/ArgValue-derive

ArgValue derive
This commit is contained in:
Ed Page 2021-10-01 08:43:40 -05:00 committed by GitHub
commit 3b2e3ffddf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 201 additions and 89 deletions

View file

@ -4,13 +4,19 @@
use clap::{ArgEnum, Clap};
#[derive(ArgEnum, Debug, PartialEq)]
#[derive(ArgEnum, Debug, PartialEq, Clone)]
enum ArgChoice {
/// Descriptions are supported as doc-comment
Foo,
// Renames are supported
#[clap(name = "b-a-r")]
Bar,
// Aliases are supported
#[clap(alias = "b", alias = "z")]
Baz,
// Hiding variants from help and completion is supported
#[clap(hidden = true)]
Hidden,
}
#[derive(Clap, PartialEq, Debug)]

View file

@ -19,7 +19,7 @@ use std::ffi::OsString;
use std::io;
use std::path::PathBuf;
#[derive(ArgEnum, Debug, PartialEq)]
#[derive(ArgEnum, Debug, PartialEq, Clone)]
enum GeneratorChoice {
Bash,
Elvish,

View file

@ -819,14 +819,6 @@ impl Attrs {
}
}
pub fn enum_aliases(&self) -> Vec<TokenStream> {
self.methods
.iter()
.filter(|m| m.name == "alias")
.map(|m| m.args.clone())
.collect()
}
pub fn casing(&self) -> Sp<CasingStyle> {
self.casing.clone()
}

View file

@ -47,9 +47,8 @@ pub fn gen_for_enum(name: &Ident, attrs: &[Attribute], e: &DataEnum) -> TokenStr
);
let lits = lits(&e.variants, &attrs);
let variants = gen_variants(&lits);
let from_str = gen_from_str(&lits);
let as_arg = gen_as_arg(&lits);
let arg_values = gen_arg_values(&lits);
let arg_value = gen_arg_value(&lits);
quote! {
#[allow(dead_code, unreachable_code, unused_variables)]
@ -65,9 +64,8 @@ pub fn gen_for_enum(name: &Ident, attrs: &[Attribute], e: &DataEnum) -> TokenStr
)]
#[deny(clippy::correctness)]
impl clap::ArgEnum for #name {
#variants
#from_str
#as_arg
#arg_values
#arg_value
}
}
}
@ -87,54 +85,35 @@ fn lits(
if let Kind::Skip(_) = &*attrs.kind() {
None
} else {
Some((variant, attrs))
let fields = attrs.field_methods();
let name = attrs.cased_name();
Some((
quote! {
clap::ArgValue::new(#name)
#fields
},
variant.ident.clone(),
))
}
})
.flat_map(|(variant, attrs)| {
let mut ret = vec![(attrs.cased_name(), variant.ident.clone())];
attrs
.enum_aliases()
.into_iter()
.for_each(|x| ret.push((x, variant.ident.clone())));
ret
})
.collect::<Vec<_>>()
}
fn gen_variants(lits: &[(TokenStream, Ident)]) -> TokenStream {
let lit = lits.iter().map(|l| &l.0).collect::<Vec<_>>();
fn gen_arg_values(lits: &[(TokenStream, Ident)]) -> TokenStream {
let lit = lits.iter().map(|l| &l.1).collect::<Vec<_>>();
quote! {
const VARIANTS: &'static [&'static str] = &[#(#lit),*];
}
}
fn gen_from_str(lits: &[(TokenStream, Ident)]) -> TokenStream {
let (lit, variant): (Vec<TokenStream>, Vec<Ident>) = lits.iter().cloned().unzip();
quote! {
fn from_str(input: &str, case_insensitive: bool) -> ::std::result::Result<Self, String> {
let func = if case_insensitive {
::std::ascii::AsciiExt::eq_ignore_ascii_case
} else {
str::eq
};
match input {
#(val if func(val, #lit) => Ok(Self::#variant),)*
e => Err(format!("Invalid variant: {}", e)),
}
fn value_variants<'a>() -> &'a [Self]{
&[#(Self::#lit),*]
}
}
}
fn gen_as_arg(lits: &[(TokenStream, Ident)]) -> TokenStream {
fn gen_arg_value(lits: &[(TokenStream, Ident)]) -> TokenStream {
let (lit, variant): (Vec<TokenStream>, Vec<Ident>) = lits.iter().cloned().unzip();
quote! {
fn as_arg(&self) -> Option<&'static str> {
fn arg_value<'a>(&self) -> Option<clap::ArgValue<'a>> {
match self {
#(Self::#variant => Some(#lit),)*
_ => None

View file

@ -361,7 +361,7 @@ pub fn gen_augment(
fn gen_arg_enum_possible_values(ty: &Type) -> TokenStream {
quote_spanned! { ty.span()=>
.possible_values(<#ty as clap::ArgEnum>::VARIANTS.into_iter().copied())
.possible_values(<#ty as clap::ArgEnum>::value_variants().iter().filter_map(clap::ArgEnum::arg_value))
}
}

View file

@ -76,11 +76,13 @@ pub fn args(name: &Ident) {
pub fn arg_enum(name: &Ident) {
append_dummy(quote! {
impl clap::ArgEnum for #name {
const VARIANTS: &'static [&'static str] = &[];
fn value_variants<'a>() -> &'a [Self]{
unimplemented!()
}
fn from_str(_input: &str, _case_insensitive: bool) -> Result<Self, String> {
unimplemented!()
}
fn as_arg(&self) -> Option<&'static str> {
fn arg_value<'a>(&self) -> Option<clap::ArgValue<'a>>{
unimplemented!()
}
}

View file

@ -7,11 +7,11 @@
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
use clap::{ArgEnum, Clap};
use clap::{ArgEnum, ArgValue, Clap};
#[test]
fn basic() {
#[derive(ArgEnum, PartialEq, Debug)]
#[derive(ArgEnum, PartialEq, Debug, Clone)]
enum ArgChoice {
Foo,
Bar,
@ -40,7 +40,7 @@ fn basic() {
#[test]
fn default_value() {
#[derive(ArgEnum, PartialEq, Debug)]
#[derive(ArgEnum, PartialEq, Debug, Clone)]
enum ArgChoice {
Foo,
Bar,
@ -54,7 +54,7 @@ fn default_value() {
impl std::fmt::Display for ArgChoice {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
std::fmt::Display::fmt(self.as_arg().unwrap(), f)
std::fmt::Display::fmt(self.arg_value().unwrap().get_name(), f)
}
}
@ -86,7 +86,7 @@ fn default_value() {
#[test]
fn multi_word_is_renamed_kebab() {
#[derive(ArgEnum, PartialEq, Debug)]
#[derive(ArgEnum, PartialEq, Debug, Clone)]
#[allow(non_camel_case_types)]
enum ArgChoice {
FooBar,
@ -116,7 +116,7 @@ fn multi_word_is_renamed_kebab() {
#[test]
fn variant_with_defined_casing() {
#[derive(ArgEnum, PartialEq, Debug)]
#[derive(ArgEnum, PartialEq, Debug, Clone)]
enum ArgChoice {
#[clap(rename_all = "screaming_snake")]
FooBar,
@ -139,7 +139,7 @@ fn variant_with_defined_casing() {
#[test]
fn casing_is_propogated_from_parent() {
#[derive(ArgEnum, PartialEq, Debug)]
#[derive(ArgEnum, PartialEq, Debug, Clone)]
#[clap(rename_all = "screaming_snake")]
enum ArgChoice {
FooBar,
@ -162,7 +162,7 @@ fn casing_is_propogated_from_parent() {
#[test]
fn casing_propogation_is_overridden() {
#[derive(ArgEnum, PartialEq, Debug)]
#[derive(ArgEnum, PartialEq, Debug, Clone)]
#[clap(rename_all = "screaming_snake")]
enum ArgChoice {
#[clap(rename_all = "camel")]
@ -187,7 +187,7 @@ fn casing_propogation_is_overridden() {
#[test]
fn case_insensitive() {
#[derive(ArgEnum, PartialEq, Debug)]
#[derive(ArgEnum, PartialEq, Debug, Clone)]
enum ArgChoice {
Foo,
}
@ -214,7 +214,7 @@ fn case_insensitive() {
#[test]
fn case_insensitive_set_to_false() {
#[derive(ArgEnum, PartialEq, Debug)]
#[derive(ArgEnum, PartialEq, Debug, Clone)]
enum ArgChoice {
Foo,
}
@ -236,7 +236,7 @@ fn case_insensitive_set_to_false() {
#[test]
fn alias() {
#[derive(ArgEnum, PartialEq, Debug)]
#[derive(ArgEnum, PartialEq, Debug, Clone)]
enum ArgChoice {
#[clap(alias = "TOTP")]
TOTP,
@ -264,7 +264,7 @@ fn alias() {
#[test]
fn multiple_alias() {
#[derive(ArgEnum, PartialEq, Debug)]
#[derive(ArgEnum, PartialEq, Debug, Clone)]
enum ArgChoice {
#[clap(alias = "TOTP", alias = "t")]
TOTP,
@ -298,7 +298,7 @@ fn multiple_alias() {
#[test]
fn option() {
#[derive(ArgEnum, PartialEq, Debug)]
#[derive(ArgEnum, PartialEq, Debug, Clone)]
enum ArgChoice {
Foo,
Bar,
@ -328,7 +328,7 @@ fn option() {
#[test]
fn vector() {
#[derive(ArgEnum, PartialEq, Debug)]
#[derive(ArgEnum, PartialEq, Debug, Clone)]
enum ArgChoice {
Foo,
Bar,
@ -358,7 +358,7 @@ fn vector() {
#[test]
fn skip_variant() {
#[derive(ArgEnum, PartialEq, Debug)]
#[derive(ArgEnum, PartialEq, Debug, Clone)]
#[allow(dead_code)] // silence warning about `Baz` being unused
enum ArgChoice {
Foo,
@ -367,7 +367,14 @@ fn skip_variant() {
Baz,
}
assert_eq!(ArgChoice::VARIANTS, ["foo", "bar"]);
assert_eq!(
ArgChoice::value_variants()
.iter()
.map(ArgEnum::arg_value)
.map(Option::unwrap)
.collect::<Vec<_>>(),
vec![ArgValue::new("foo"), ArgValue::new("bar")]
);
assert!(ArgChoice::from_str("foo", true).is_ok());
assert!(ArgChoice::from_str("bar", true).is_ok());
assert!(ArgChoice::from_str("baz", true).is_err());
@ -375,7 +382,7 @@ fn skip_variant() {
#[test]
fn from_str_invalid() {
#[derive(ArgEnum, PartialEq, Debug)]
#[derive(ArgEnum, PartialEq, Debug, Clone)]
enum ArgChoice {
Foo,
}

View file

@ -1,8 +1,8 @@
use clap::ArgEnum;
#[derive(ArgEnum, Debug)]
#[derive(ArgEnum, Clone, Debug)]
struct Opt {}
fn main() {
println!("{:?}", Opt::VARIANTS);
println!("{:?}", Opt::value_variants());
}

View file

@ -1,7 +1,7 @@
error: `#[derive(ArgEnum)]` only supports enums
--> $DIR/arg_enum_on_struct.rs:3:10
|
3 | #[derive(ArgEnum, Debug)]
3 | #[derive(ArgEnum, Clone, Debug)]
| ^^^^^^^
|
= note: this error originates in the derive macro `ArgEnum` (in Nightly builds, run with -Z macro-backtrace for more info)

View file

@ -1,3 +1,5 @@
use std::iter;
/// The representation of a possible value of an argument.
///
/// This is used for specifying [possible values] of [Args].
@ -20,10 +22,11 @@
/// [possible values]: crate::Arg::possible_value()
/// [hide]: ArgValue::hidden()
/// [about]: ArgValue::about()
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct ArgValue<'help> {
pub(crate) name: &'help str,
pub(crate) about: Option<&'help str>,
pub(crate) aliases: Vec<&'help str>, // (name, visible)
pub(crate) hidden: bool,
}
@ -61,6 +64,37 @@ impl<'help> ArgValue<'help> {
Some(self.name)
}
}
/// Returns all valid values of the argument value.
/// Namely the name and all aliases.
pub fn get_name_and_aliases(&self) -> impl Iterator<Item = &str> {
iter::once(&self.name).chain(&self.aliases).copied()
}
/// Tests if the value is valid for this argument value
///
/// The value is valid if it is either the name or one of the aliases.
///
/// # Examples
///
/// ```rust
/// # use clap::ArgValue;
/// let arg_value = ArgValue::new("fast").alias("not-slow");
///
/// assert!(arg_value.matches("fast", false));
/// assert!(arg_value.matches("not-slow", false));
///
/// assert!(arg_value.matches("FAST", true));
/// assert!(!arg_value.matches("FAST", false));
/// ```
pub fn matches(&self, value: &str, ignore_case: bool) -> bool {
if ignore_case {
self.get_name_and_aliases()
.any(|name| name.eq_ignore_ascii_case(value))
} else {
self.get_name_and_aliases().any(|name| name == value)
}
}
}
impl<'help> ArgValue<'help> {
@ -123,4 +157,41 @@ impl<'help> ArgValue<'help> {
self.hidden = yes;
self
}
/// Sets an alias for this argument value.
///
/// The alias will be hidden from completion and help texts.
///
/// # Examples
///
/// ```rust
/// # use clap::ArgValue;
/// ArgValue::new("slow")
/// .alias("not-fast")
/// # ;
/// ```
pub fn alias(mut self, name: &'help str) -> Self {
self.aliases.push(name);
self
}
/// Sets multiple aliases for this argument value.
///
/// The aliases will be hidden from completion and help texts.
///
/// # Examples
///
/// ```rust
/// # use clap::ArgValue;
/// ArgValue::new("slow")
/// .aliases(["not-fast", "snake-like"])
/// # ;
/// ```
pub fn aliases<I>(mut self, names: I) -> Self
where
I: IntoIterator<Item = &'help str>,
{
self.aliases.extend(names.into_iter());
self
}
}

View file

@ -1,7 +1,7 @@
//! This module contains traits that are usable with the `#[derive(...)].`
//! macros in [`clap_derive`].
use crate::{App, ArgMatches, Error};
use crate::{App, ArgMatches, ArgValue, Error};
use std::ffi::OsString;
@ -273,7 +273,7 @@ pub trait Subcommand: FromArgMatches + Sized {
/// level: Level,
/// }
///
/// #[derive(clap::ArgEnum)]
/// #[derive(clap::ArgEnum, Clone)]
/// enum Level {
/// Debug,
/// Info,
@ -281,17 +281,23 @@ pub trait Subcommand: FromArgMatches + Sized {
/// Error,
/// }
/// ```
pub trait ArgEnum: Sized {
/// All possible argument choices, in display order.
const VARIANTS: &'static [&'static str];
pub trait ArgEnum: Sized + Clone {
/// All possible argument values, in display order.
fn value_variants<'a>() -> &'a [Self];
/// Parse an argument into `Self`.
fn from_str(input: &str, case_insensitive: bool) -> Result<Self, String>;
fn from_str(input: &str, case_insensitive: bool) -> Result<Self, String> {
Self::value_variants()
.iter()
.find(|v| v.arg_value().unwrap().matches(input, case_insensitive))
.cloned()
.ok_or_else(|| format!("Invalid variant: {}", input))
}
/// The canonical argument value.
///
/// The value is `None` for skipped variants.
fn as_arg(&self) -> Option<&'static str>;
fn arg_value<'a>(&self) -> Option<ArgValue<'a>>;
}
impl<T: Clap> Clap for Box<T> {

View file

@ -104,13 +104,10 @@ impl<'help, 'app, 'parser> Validator<'help, 'app, 'parser> {
arg.possible_vals
);
let val_str = val.to_string_lossy();
let ok = if arg.is_set(ArgSettings::IgnoreCase) {
arg.possible_vals
.iter()
.any(|pv| pv.name.eq_ignore_ascii_case(&val_str))
} else {
arg.possible_vals.iter().any(|pv| pv.name == &*val_str)
};
let ok = arg
.possible_vals
.iter()
.any(|pv| pv.matches(&val_str, arg.is_set(ArgSettings::IgnoreCase)));
if !ok {
let used: Vec<Id> = matcher
.arg_names()

View file

@ -219,6 +219,22 @@ fn possible_values_output() {
));
}
#[test]
fn possible_values_alias_output() {
assert!(utils::compare_output(
App::new("test").arg(
Arg::new("option")
.short('O')
.possible_value("slow")
.possible_value(ArgValue::new("fast").alias("fost"))
.possible_value(ArgValue::new("ludicrous speed").aliases(["ls", "lcs"]))
),
"clap-test -O slo",
PV_ERROR,
true
));
}
#[test]
fn possible_values_hidden_output() {
assert!(utils::compare_output(
@ -249,6 +265,42 @@ fn escaped_possible_values_output() {
));
}
#[test]
fn alias() {
let m = App::new("pv")
.arg(
Arg::new("option")
.short('o')
.long("--option")
.takes_value(true)
.possible_value(ArgValue::new("test123").alias("123"))
.possible_value("test321")
.case_insensitive(true),
)
.try_get_matches_from(vec!["pv", "--option", "123"]);
assert!(m.is_ok());
assert!(m.unwrap().value_of("option").unwrap().eq("123"));
}
#[test]
fn aliases() {
let m = App::new("pv")
.arg(
Arg::new("option")
.short('o')
.long("--option")
.takes_value(true)
.possible_value(ArgValue::new("test123").aliases(["1", "2", "3"]))
.possible_value("test321")
.case_insensitive(true),
)
.try_get_matches_from(vec!["pv", "--option", "2"]);
assert!(m.is_ok());
assert!(m.unwrap().value_of("option").unwrap().eq("2"));
}
#[test]
fn case_insensitive() {
let m = App::new("pv")
@ -272,7 +324,7 @@ fn case_insensitive() {
}
#[test]
fn case_insensitive_faili() {
fn case_insensitive_fail() {
let m = App::new("pv")
.arg(
Arg::new("option")