Added arg_enum support

This commit is contained in:
Pavan Kumar Sunkara 2020-04-22 09:25:41 +02:00
parent c1ab22c3c9
commit 01c179f527
20 changed files with 388 additions and 272 deletions

View file

@ -21,10 +21,6 @@ as soon as [this](https://github.com/rust-lang/rust/issues/24584) is fixed (if e
How to use doc comments in place of `help/long_help`.
### [Enums as arguments](enum_in_args.rs)
How to use `arg_enum!` with `clap_derive`.
### [Arguments of subcommands in separate `struct`](enum_tuple.rs)
How to extract subcommands' args into external structs.
@ -75,4 +71,4 @@ How to express "`"true"` or `"false"` argument.
### [Author, description, and version from `Cargo.toml`](from_crate.rs)
//! How to derive a author, description, and version from Cargo.toml
How to derive a author, description, and version from Cargo.toml

View file

@ -0,0 +1,19 @@
use clap::Clap;
#[derive(Clap, Debug, PartialEq)]
enum ArgChoice {
Foo,
Bar,
Baz,
}
#[derive(Clap, PartialEq, Debug)]
struct Opt {
#[clap(arg_enum)]
arg: ArgChoice,
}
fn main() {
let opt = Opt::parse();
println!("{:#?}", opt);
}

View file

@ -1,28 +0,0 @@
// #[macro_use]
// extern crate clap;
// use clap::{App, Arg};
fn main() {}
// #[derive(ArgEnum, Debug)]
// enum ArgChoice {
// Foo,
// Bar,
// Baz,
// }
// fn main() {
// let matches = App::new(env!("CARGO_PKG_NAME"))
// .arg(
// Arg::with_name("arg")
// .required(true)
// .takes_value(true)
// .possible_values(&ArgChoice::variants()),
// )
// .get_matches();
// let t = value_t!(matches.value_of("arg"), ArgChoice).unwrap_or_else(|e| e.exit());
// println!("{:?}", t);
// }

View file

@ -1,28 +0,0 @@
fn main() {}
// #[macro_use]
// extern crate clap;
// use clap::{App, Arg};
// #[derive(ArgEnum, Debug)]
// #[case_sensitive]
// enum ArgChoice {
// Foo,
// Bar,
// Baz,
// }
// fn main() {
// let matches = App::new(env!("CARGO_PKG_NAME"))
// .arg(
// Arg::with_name("arg")
// .required(true)
// .takes_value(true)
// .possible_values(&ArgChoice::variants()),
// )
// .get_matches();
// let t = value_t!(matches.value_of("arg"), ArgChoice).unwrap_or_else(|e| e.exit());
// println!("{:?}", t);
// }

View file

@ -1,25 +0,0 @@
//! How to use `arg_enum!` with `StructOpt`.
// TODO: make it work
fn main() {}
// use clap::Clap;
// arg_enum! {
// #[derive(Debug)]
// enum Baz {
// Foo,
// Bar,
// FooBar
// }
// }
// #[derive(Clap, Debug)]
// struct Opt {
// /// Important argument.
// #[clap(possible_values = &Baz::variants(), case_insensitive = true)]
// i: Baz,
// }
// fn main() {
// let opt = Opt::parse();
// println!("{:?}", opt);
// }

View file

@ -7,86 +7,86 @@
// <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 proc_macro2;
use quote;
use syn;
use syn::punctuated;
use syn::token;
use crate::derives::attrs::{Attrs, Name, DEFAULT_CASING, DEFAULT_ENV_CASING};
use crate::derives::spanned::Sp;
pub fn derive_arg_enum(_ast: &syn::DeriveInput) -> proc_macro2::TokenStream {
let from_str_block = impl_from_str(ast)?;
let variants_block = impl_variants(ast)?;
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::{
punctuated::Punctuated, spanned::Spanned, token::Comma, Attribute, DataEnum, Ident, Variant,
};
pub fn gen_for_enum(name: &Ident, attrs: &[Attribute], e: &DataEnum) -> TokenStream {
let attrs = Attrs::from_struct(
Span::call_site(),
attrs,
Name::Derived(name.clone()),
Sp::call_site(DEFAULT_CASING),
Sp::call_site(DEFAULT_ENV_CASING),
);
let lits = lits(&e.variants, &attrs);
let variants = gen_variants(&lits);
let from_str = gen_from_str(&e.variants, &lits);
quote! {
#from_str_block
#variants_block
#[allow(dead_code, unreachable_code, unused_variables)]
#[allow(
clippy::style,
clippy::complexity,
clippy::pedantic,
clippy::restriction,
clippy::perf,
clippy::deprecated,
clippy::nursery,
clippy::cargo
)]
#[deny(clippy::correctness)]
impl ::clap::ArgEnum for #name {
#variants
#from_str
}
}
}
fn impl_from_str(ast: &syn::DeriveInput) -> proc_macro2::TokenStream {
let ident = &ast.ident;
let is_case_sensitive = ast.attrs.iter().any(|v| v.name() == "case_sensitive");
let variants = variants(ast)?;
let strings = variants
fn lits(variants: &Punctuated<Variant, Comma>, parent_attribute: &Attrs) -> Vec<TokenStream> {
variants
.iter()
.map(|ref variant| String::from(variant.ident.as_ref()))
.collect::<Vec<_>>();
.map(|variant| {
let attrs = Attrs::from_struct(
variant.span(),
&variant.attrs,
Name::Derived(variant.ident.clone()),
parent_attribute.casing(),
parent_attribute.env_casing(),
);
// All of these need to be iterators.
let ident_slice = [ident.clone()];
let idents = ident_slice.iter().cycle();
let for_error_message = strings.clone();
let condition_function_slice = [match is_case_sensitive {
true => quote! { str::eq },
false => quote! { ::std::ascii::AsciiExt::eq_ignore_ascii_case },
}];
let condition_function = condition_function_slice.iter().cycle();
Ok(quote! {
impl ::std::str::FromStr for #ident {
type Err = String;
fn from_str(input: &str) -> ::std::result::Result<Self, Self::Err> {
match input {
#(val if #condition_function(val, #strings) => Ok(#idents::#variants),)*
_ => Err({
let v = #for_error_message;
format!("valid values: {}",
v.join(" ,"))
}),
}
}
}
})
attrs.cased_name()
})
.collect::<Vec<_>>()
}
fn impl_variants(ast: &syn::DeriveInput) -> proc_macro2::TokenStream {
let ident = &ast.ident;
let variants = variants(ast)?
.iter()
.map(|ref variant| String::from(variant.ident.as_ref()))
.collect::<Vec<_>>();
let length = variants.len();
Ok(quote! {
impl #ident {
fn variants() -> [&'static str; #length] {
#variants
}
}
})
}
fn variants(ast: &syn::DeriveInput) -> &punctuated::Punctuated<syn::Variant, token::Comma> {
use syn::Data::*;
match ast.data {
Enum(ref data) => data.variants,
_ => panic!("Only enums are supported for deriving the ArgEnum trait"),
fn gen_variants(lits: &Vec<TokenStream>) -> TokenStream {
quote! {
const VARIANTS: &'static [&'static str] = &[#(#lits),*];
}
}
fn gen_from_str(variants: &Punctuated<Variant, Comma>, lits: &Vec<TokenStream>) -> TokenStream {
let matches = variants.iter().map(|v| &v.ident).collect::<Vec<_>>();
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, #lits) => Ok(Self::#matches),)*
_ => Err(String::from("something went wrong parsing the value")),
}
}
}
}
*/

View file

@ -98,6 +98,7 @@ pub struct Attrs {
about: Option<Method>,
version: Option<Method>,
verbatim_doc_comment: Option<Ident>,
is_enum: bool,
has_custom_parser: bool,
kind: Sp<Kind>,
}
@ -260,7 +261,7 @@ impl Attrs {
author: None,
version: None,
verbatim_doc_comment: None,
is_enum: false,
has_custom_parser: false,
kind: Sp::new(Kind::Arg(Sp::new(Ty::Other, default_span)), default_span),
}
@ -289,6 +290,8 @@ impl Attrs {
self.push_method(ident, self.name.clone().translate(*self.env_casing));
}
ArgEnum(_) => self.is_enum = true,
Subcommand(ident) => {
let ty = Sp::call_site(Ty::Other);
let kind = Sp::new(Kind::Subcommand(ty), ident.span());
@ -514,6 +517,9 @@ impl Attrs {
note = "see also https://github.com/clap-rs/clap_derive/tree/master/examples/true_or_false.rs";
)
}
if res.is_enum {
abort!(field.ty, "enum is meaningless for bool")
}
if let Some(m) = res.find_method("default_value") {
abort!(m.name, "default_value is meaningless for bool")
}
@ -610,6 +616,20 @@ impl Attrs {
self.kind.clone()
}
pub fn is_enum(&self) -> bool {
self.is_enum
}
pub fn case_insensitive(&self) -> TokenStream {
let method = self.find_method("case_insensitive");
if let Some(method) = method {
method.args.clone()
} else {
quote! { false }
}
}
pub fn casing(&self) -> Sp<CasingStyle> {
self.casing.clone()
}

View file

@ -12,7 +12,7 @@
// commit#ea76fa1b1b273e65e3b0b1046643715b49bec51f which is licensed under the
// MIT/Apache 2.0 license.
use super::{dummies, from_argmatches, into_app, subcommand};
use super::{arg_enum, dummies, from_argmatches, into_app, subcommand};
use proc_macro2::TokenStream;
use proc_macro_error::abort_call_site;
use quote::quote;
@ -70,11 +70,25 @@ fn gen_for_enum(name: &Ident, attrs: &[Attribute], e: &DataEnum) -> TokenStream
let into_app = into_app::gen_for_enum(name);
let from_arg_matches = from_argmatches::gen_for_enum(name);
let subcommand = subcommand::gen_for_enum(name, attrs, e);
let arg_enum = if e.variants.iter().all(|v| {
if let syn::Fields::Unit = v.fields {
true
} else {
false
}
}) {
arg_enum::gen_for_enum(name, attrs, e)
} else {
quote!()
};
quote! {
impl ::clap::Clap for #name { }
#into_app
#from_arg_matches
#subcommand
#arg_enum
}
}

View file

@ -14,6 +14,7 @@ pub fn clap_enum(name: &Ident) {
into_app(name);
from_arg_matches(name);
subcommand(name);
arg_enum(name);
append_dummy(quote!( impl ::clap::Clap for #name {} ));
}
@ -52,3 +53,14 @@ pub fn subcommand(name: &Ident) {
}
});
}
pub fn arg_enum(name: &Ident) {
append_dummy(quote! {
impl ::clap::ArgEnum for #name {
const VARIANTS: &'static [&'static str] = &[];
fn from_str(_input: &str, _case_insensitive: bool) -> Result<Self, String> {
unimplemented!()
}
}
})
}

View file

@ -192,11 +192,24 @@ pub fn gen_constructor(
#parse(matches.is_present(#name))
},
Ty::Other => quote_spanned! { ty.span()=>
matches.#value_of(#name)
.map(#parse)
.unwrap()
},
Ty::Other => {
let parse = if attrs.is_enum() {
let field_ty = &field.ty;
let ci = attrs.case_insensitive();
quote! {
|s| <#field_ty as ::clap::ArgEnum>::from_str(s, #ci).unwrap()
}
} else {
parse
};
quote_spanned! { ty.span()=>
matches.#value_of(#name)
.map(#parse)
.unwrap()
}
}
};
quote_spanned!(field.span()=> #field_name: #field_value )

View file

@ -196,6 +196,7 @@ pub fn gen_app_augmentation(
let parser = attrs.parser();
let func = &parser.func;
let validator = match *parser.kind {
_ if attrs.is_enum() => quote!(),
ParserKind::TryFromStr => quote_spanned! { func.span()=>
.validator(|s| {
#func(s.as_str())
@ -249,9 +250,21 @@ pub fn gen_app_augmentation(
Ty::Other => {
let required = !attrs.has_method("default_value");
let possible_values = if attrs.is_enum() {
let field_ty = &field.ty;
quote! {
.possible_values(&<#field_ty as ::clap::ArgEnum>::VARIANTS)
}
} else {
quote!()
};
quote_spanned! { ty.span()=>
.takes_value(true)
.required(#required)
#possible_values
#validator
}
}

View file

@ -11,7 +11,7 @@
// This work was derived from Structopt (https://github.com/TeXitoi/structopt)
// commit#ea76fa1b1b273e65e3b0b1046643715b49bec51f which is licensed under the
// MIT/Apache 2.0 license.
pub mod arg_enum;
mod arg_enum;
pub mod attrs;
mod clap;
mod doc_comments;

View file

@ -16,6 +16,7 @@ pub enum ClapAttr {
Long(Ident),
Env(Ident),
Flatten(Ident),
ArgEnum(Ident),
Subcommand(Ident),
VerbatimDocComment(Ident),
@ -167,6 +168,7 @@ impl Parse for ClapAttr {
"short" => Ok(Short(name)),
"env" => Ok(Env(name)),
"flatten" => Ok(Flatten(name)),
"arg_enum" => Ok(ArgEnum(name)),
"subcommand" => Ok(Subcommand(name)),
"verbatim_doc_comment" => Ok(VerbatimDocComment(name)),

View file

@ -0,0 +1,189 @@
// Copyright 2018 Guillaume Pinot (@TeXitoi) <texitoi@texitoi.eu>,
// Kevin Knapp (@kbknapp) <kbknapp@gmail.com>, and
// Andrew Hobden (@hoverbear) <andrew@hoverbear.org>
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <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::Clap;
#[test]
fn basic() {
#[derive(Clap, PartialEq, Debug)]
enum ArgChoice {
Foo,
Bar,
}
#[derive(Clap, PartialEq, Debug)]
struct Opt {
#[clap(arg_enum)]
arg: ArgChoice,
};
assert_eq!(
Opt {
arg: ArgChoice::Foo
},
Opt::parse_from(&["", "foo"])
);
assert_eq!(
Opt {
arg: ArgChoice::Bar
},
Opt::parse_from(&["", "bar"])
);
assert!(Opt::try_parse_from(&["", "fOo"]).is_err());
}
#[test]
fn multi_word_is_renamed_kebab() {
#[derive(Clap, PartialEq, Debug)]
#[allow(non_camel_case_types)]
enum ArgChoice {
FooBar,
BAR_BAZ,
}
#[derive(Clap, PartialEq, Debug)]
struct Opt {
#[clap(arg_enum)]
arg: ArgChoice,
};
assert_eq!(
Opt {
arg: ArgChoice::FooBar
},
Opt::parse_from(&["", "foo-bar"])
);
assert_eq!(
Opt {
arg: ArgChoice::BAR_BAZ
},
Opt::parse_from(&["", "bar-baz"])
);
assert!(Opt::try_parse_from(&["", "FooBar"]).is_err());
}
#[test]
fn variant_with_defined_casing() {
#[derive(Clap, PartialEq, Debug)]
enum ArgChoice {
#[clap(rename_all = "screaming_snake")]
FooBar,
}
#[derive(Clap, PartialEq, Debug)]
struct Opt {
#[clap(arg_enum)]
arg: ArgChoice,
};
assert_eq!(
Opt {
arg: ArgChoice::FooBar
},
Opt::parse_from(&["", "FOO_BAR"])
);
assert!(Opt::try_parse_from(&["", "FooBar"]).is_err());
}
#[test]
fn casing_is_propogated_from_parent() {
#[derive(Clap, PartialEq, Debug)]
#[clap(rename_all = "screaming_snake")]
enum ArgChoice {
FooBar,
}
#[derive(Clap, PartialEq, Debug)]
struct Opt {
#[clap(arg_enum)]
arg: ArgChoice,
};
assert_eq!(
Opt {
arg: ArgChoice::FooBar
},
Opt::parse_from(&["", "FOO_BAR"])
);
assert!(Opt::try_parse_from(&["", "FooBar"]).is_err());
}
#[test]
fn casing_propogation_is_overridden() {
#[derive(Clap, PartialEq, Debug)]
#[clap(rename_all = "screaming_snake")]
enum ArgChoice {
#[clap(rename_all = "camel")]
FooBar,
}
#[derive(Clap, PartialEq, Debug)]
struct Opt {
#[clap(arg_enum)]
arg: ArgChoice,
};
assert_eq!(
Opt {
arg: ArgChoice::FooBar
},
Opt::parse_from(&["", "fooBar"])
);
assert!(Opt::try_parse_from(&["", "FooBar"]).is_err());
assert!(Opt::try_parse_from(&["", "FOO_BAR"]).is_err());
}
#[test]
fn case_insensitive() {
#[derive(Clap, PartialEq, Debug)]
enum ArgChoice {
Foo,
}
#[derive(Clap, PartialEq, Debug)]
struct Opt {
#[clap(arg_enum, case_insensitive(true))]
arg: ArgChoice,
};
assert_eq!(
Opt {
arg: ArgChoice::Foo
},
Opt::parse_from(&["", "foo"])
);
assert_eq!(
Opt {
arg: ArgChoice::Foo
},
Opt::parse_from(&["", "fOo"])
);
}
#[test]
fn case_insensitive_set_to_false() {
#[derive(Clap, PartialEq, Debug)]
enum ArgChoice {
Foo,
}
#[derive(Clap, PartialEq, Debug)]
struct Opt {
#[clap(arg_enum, case_insensitive(false))]
arg: ArgChoice,
};
assert_eq!(
Opt {
arg: ArgChoice::Foo
},
Opt::parse_from(&["", "foo"])
);
assert!(Opt::try_parse_from(&["", "fOo"]).is_err());
}

View file

@ -1,54 +0,0 @@
// // Copyright 2018 Guillaume Pinot (@TeXitoi) <texitoi@texitoi.eu>,
// // Kevin Knapp (@kbknapp) <kbknapp@gmail.com>, and
// // Andrew Hobden (@hoverbear) <andrew@hoverbear.org>
// //
// // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// // <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.
// #[macro_use]
// extern crate clap;
// #[macro_use]
// extern crate clap_derive;
// use clap::{App, Arg};
// #[derive(ArgEnum, Debug, PartialEq)]
// enum ArgChoice {
// Foo,
// Bar,
// Baz,
// }
// #[test]
// fn when_lowercase() {
// let matches = App::new(env!("CARGO_PKG_NAME"))
// .arg(
// Arg::with_name("arg")
// .required(true)
// .takes_value(true)
// .possible_values(&ArgChoice::variants()),
// )
// .try_get_matches_from(vec!["", "foo"])
// .unwrap();
// let t = value_t!(matches.value_of("arg"), ArgChoice);
// assert!(t.is_ok());
// assert_eq!(t.unwrap(), ArgChoice::Foo);
// }
// #[test]
// fn when_capitalized() {
// let matches = App::new(env!("CARGO_PKG_NAME"))
// .arg(
// Arg::with_name("arg")
// .required(true)
// .takes_value(true)
// .possible_values(&ArgChoice::variants()),
// )
// .try_get_matches_from(vec!["", "Foo"])
// .unwrap();
// let t = value_t!(matches.value_of("arg"), ArgChoice);
// assert!(t.is_ok());
// assert_eq!(t.unwrap(), ArgChoice::Foo);
// }

View file

@ -1,51 +0,0 @@
// // Copyright 2018 Guillaume Pinot (@TeXitoi) <texitoi@texitoi.eu>,
// // Kevin Knapp (@kbknapp) <kbknapp@gmail.com>, and
// // Andrew Hobden (@hoverbear) <andrew@hoverbear.org>
// //
// // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// // <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.
// #[macro_use]
// extern crate clap;
// use clap::{App, Arg, ArgEnum};
// #[derive(ArgEnum, Debug, PartialEq)]
// #[case_sensitive]
// enum ArgChoice {
// Foo,
// Bar,
// Baz,
// }
// #[test]
// fn when_lowercase() {
// let matches = App::new(env!("CARGO_PKG_NAME"))
// .arg(
// Arg::with_name("arg")
// .required(true)
// .takes_value(true)
// .possible_values(&ArgChoice::variants()),
// )
// .try_get_matches_from(vec!["", "foo"]); // We expect this to fail.
// assert!(matches.is_err());
// assert_eq!(matches.unwrap_err().kind, clap::ErrorKind::InvalidValue);
// }
// #[test]
// fn when_capitalized() {
// let matches = App::new(env!("CARGO_PKG_NAME"))
// .arg(
// Arg::with_name("arg")
// .required(true)
// .takes_value(true)
// .possible_values(&ArgChoice::variants()),
// )
// .try_get_matches_from(vec!["", "Foo"])
// .unwrap();
// let t = value_t!(matches.value_of("arg"), ArgChoice);
// assert!(t.is_ok());
// assert_eq!(t.unwrap(), ArgChoice::Foo);
// }

View file

@ -0,0 +1,13 @@
use clap::Clap;
#[derive(Clap, Debug)]
#[clap(name = "basic")]
struct Opt {
#[clap(short, arg_enum)]
opts: bool,
}
fn main() {
let opt = Opt::parse();
println!("{:?}", opt);
}

View file

@ -0,0 +1,5 @@
error: enum is meaningless for bool
--> $DIR/bool_arg_enum.rs:7:11
|
7 | opts: bool,
| ^^^^

View file

@ -65,7 +65,13 @@ pub trait Subcommand: Sized {
}
/// @TODO @release @docs
pub trait ArgEnum {}
pub trait ArgEnum: Sized {
/// @TODO @release @docs
const VARIANTS: &'static [&'static str];
/// @TODO @release @docs
fn from_str(input: &str, case_insensitive: bool) -> Result<Self, String>;
}
impl<T: Clap> Clap for Box<T> {
fn parse() -> Self {

View file

@ -448,7 +448,7 @@
compile_error!("`std` feature is currently required to build `clap`");
pub use crate::build::{App, AppSettings, Arg, ArgGroup, ArgSettings};
pub use crate::derive::{Clap, FromArgMatches, IntoApp, Subcommand};
pub use crate::derive::{ArgEnum, Clap, FromArgMatches, IntoApp, Subcommand};
pub use crate::parse::errors::{Error, ErrorKind, Result};
pub use crate::parse::{ArgMatches, OsValues, SubCommand, Values};