basic help flag

This commit is contained in:
Terts Diepraam
2022-12-10 15:54:31 +01:00
parent 6b7c612431
commit 77fe0e9b9d
8 changed files with 707 additions and 357 deletions
+371
View File
@@ -0,0 +1,371 @@
use std::ops::RangeInclusive;
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Attribute, Fields, FieldsUnnamed, Lit, Meta, Variant, Ident, punctuated::Punctuated, LitStr, Token};
use crate::{
attributes::{parse_argument_attribute, ArgAttr},
Arg,
};
pub(crate) struct Argument {
ident: Ident,
name: String,
arg_type: ArgType,
help: String,
}
pub(crate) enum TakesValue {
Yes,
Optional,
No,
}
pub(crate) enum ArgType {
Option {
short_flags: Vec<char>,
long_flags: Vec<String>,
takes_value: TakesValue,
},
Positional {
num_args: RangeInclusive<usize>,
},
}
pub(crate) fn parse_help_flags(attrs: &[Attribute]) -> (Vec<char>, Vec<String>) {
for attr in attrs {
if attr.path.is_ident("help") {
let mut short = Vec::new();
let mut long = Vec::new();
for s in attr.parse_args_with(Punctuated::<LitStr, Token![,]>::parse_terminated).unwrap() {
let s = s.value().to_string();
if let Some(s) = s.strip_prefix("--") {
long.push(s.to_string());
} else if let Some(s) = s.strip_prefix("-") {
assert_eq!(s.len(), 1);
short.push(s.chars().next().unwrap())
}
return (short, long);
}
}
}
return (vec!['h'], vec!["help".into()])
}
pub(crate) fn parse_argument(v: Variant) -> Option<Argument> {
let ident = v.ident;
let name = ident.to_string();
let attribute = get_arg_attribute(&v.attrs)?;
let help = collect_help(&v.attrs);
let field = match v.fields {
Fields::Unit => None,
Fields::Unnamed(FieldsUnnamed { unnamed, .. }) => {
let v: Vec<_> = unnamed.iter().collect();
assert!(
v.len() == 1,
"Variants in an Arguments enum can have at most 1 field."
);
Some(v[0].ty.clone())
}
Fields::Named(_) => {
panic!("Named fields are not supported in Arguments");
}
};
let arg_type = match attribute {
ArgAttr::Option(opt) => {
let (short_flags, long_flags) = flag_names(opt.flags, &name);
let takes_value = match field {
None => TakesValue::No,
Some(x) if type_is_option(&x) => TakesValue::Optional,
Some(_) => TakesValue::Yes,
};
ArgType::Option {
short_flags,
long_flags,
takes_value,
}
}
ArgAttr::Positional(pos) => {
assert!(field.is_some(), "Positional arguments must have a field");
ArgType::Positional {
num_args: pos.num_args,
}
}
};
Some(Argument {
ident,
name,
arg_type,
help,
})
}
fn collect_help(attrs: &[Attribute]) -> String {
let mut help = Vec::new();
for attr in attrs {
let Ok(meta) = attr.parse_meta() else { continue; };
let Meta::NameValue(name_value) = meta else { continue; };
if !name_value.path.is_ident("doc") {
continue;
}
let Lit::Str(litstr) = name_value.lit else { continue; };
help.push(litstr.value().trim().to_string())
}
help.join(" ")
}
fn get_arg_attribute(attrs: &[Attribute]) -> Option<ArgAttr> {
let attrs: Vec<_> = attrs
.iter()
.filter(|a| a.path.is_ident("option") || a.path.is_ident("positional"))
.collect();
match attrs[..] {
[] => None,
[attr] => Some(parse_argument_attribute(attr)),
_ => panic!("Can only specify one #[option] or #[positional] per argument variant"),
}
}
fn type_is_option(syn_type: &syn::Type) -> bool {
if let syn::Type::Path(field_type_path) = syn_type {
field_type_path
.path
.segments
.iter()
.map(|s| s.ident.to_string())
.collect::<Vec<_>>()
== vec!["Option"]
} else {
false
}
}
fn flag_names(flags: Vec<Arg>, field_name: &str) -> (Vec<char>, Vec<String>) {
let field_name = field_name.to_lowercase();
if flags.is_empty() {
let first_char = field_name.chars().next().unwrap();
if field_name.len() > 1 {
(vec![first_char], vec![field_name.to_string()])
} else {
(vec![first_char], vec![])
}
} else {
let mut shorts = Vec::new();
let mut longs = Vec::new();
for flag in flags {
match flag {
Arg::Short(x) => shorts.push(x),
Arg::Long(x) => longs.push(x),
};
}
(shorts, longs)
}
}
pub(crate) fn short_handling(args: &[Argument]) -> TokenStream {
let mut match_arms = Vec::new();
for arg in args {
let ArgType::Option { ref short_flags, .. } = arg.arg_type else {
continue;
};
if short_flags.is_empty() {
continue;
}
let expr = argument_expression(arg);
match_arms.push(quote!(#(#short_flags)|* => { #expr }))
}
quote!(
match short {
#(#match_arms)*
_ => return Err(arg.unexpected().into()),
}
)
}
pub(crate) fn long_handling(args: &[Argument]) -> TokenStream {
let mut match_arms = Vec::new();
let mut options = Vec::new();
for arg in args {
let ArgType::Option { ref long_flags, .. } = arg.arg_type else {
continue;
};
if long_flags.is_empty() {
continue;
}
let expr = argument_expression(arg);
match_arms.push(quote!(#(#long_flags)|* => { #expr }));
options.extend(long_flags);
}
if options.is_empty() {
return quote!(return Err(arg.unexpected().into()));
}
let num_opts = options.len();
quote!(
let long_options: [&str; #num_opts] = [#(#options),*];
let mut candidates = Vec::new();
let mut exact_match = None;
for opt in long_options {
if opt == long {
exact_match = Some(opt);
break;
} else if opt.starts_with(long) {
candidates.push(opt);
}
}
let opt = match (exact_match, &candidates[..]) {
(Some(opt), _) => opt,
(None, [opt]) => opt,
(None, []) => return Err(arg.unexpected().into()),
(None, opts) => return Err(Error::AmbiguousOption {
option: long.to_string(),
candidates: candidates.iter().map(|s| s.to_string()).collect(),
})
};
match opt {
#(#match_arms)*
_ => unreachable!("Should be caught by (None, []) case above.")
}
)
}
pub(crate) fn positional_handling(args: &[Argument]) -> (TokenStream, TokenStream) {
let mut match_arms = Vec::new();
// The largest index of the previous argument, so the the argument after this should
// belong to the next argument.
let mut last_index = 0;
// The minimum number of arguments needed to not return a missing argument error.
let mut minimum_needed = 0;
let mut missing_argument_checks = vec![];
for arg @ Argument { name, arg_type, .. } in args {
let ArgType::Positional{ ref num_args } = arg_type else {
continue;
};
if *num_args.start() > 0 {
minimum_needed = last_index + num_args.start();
missing_argument_checks.push(quote!(if positional_idx < #minimum_needed {
missing.push(#name);
}));
}
last_index += num_args.end();
let expr = argument_expression(arg);
match_arms.push(quote!(0..=#last_index => { #expr }));
}
let value_handling = quote!(
*positional_idx += 1;
match positional_idx {
#(#match_arms)*
_ => return Err(lexopt::Arg::Value(value).unexpected().into()),
}
);
let missing_argument_checks = quote!(
// We have the minimum number of required arguments overall.
// So we don't need to check the others.
if positional_idx >= #minimum_needed {
return Ok(());
}
let mut missing: Vec<&str> = vec![];
#(#missing_argument_checks)*
if !missing.is_empty() {
Err(uutils_args::Error::MissingPositionalArguments(
missing.iter().map(ToString::to_string).collect::<Vec<String>>()
))
} else {
Ok(())
}
);
(value_handling, missing_argument_checks)
}
fn argument_expression(arg: &Argument) -> TokenStream {
let Argument { ident, arg_type, .. } = arg;
match arg_type {
ArgType::Option { takes_value, .. } => match takes_value {
TakesValue::No => quote!(Self::#ident),
TakesValue::Optional => quote!(
Self::#ident(match parser.optional_value() {
Some(x) => Some(FromValue::from_value(x)?),
None => None,
})
),
TakesValue::Yes => quote!(
Self::#ident(FromValue::from_value(parser.value()?)?)
),
}
ArgType::Positional { .. } => quote!(
Self::#ident(FromValue::from_value(value)?)
),
}
}
pub(crate) fn help_handling(args: &[Argument], short_help_flags: &[char], long_help_flags: &[String]) -> String {
let mut options = Vec::new();
let width = 16;
let indent = 2;
for Argument { arg_type, help, ..} in args {
match arg_type {
ArgType::Option { short_flags, long_flags, ..} => {
let flags = format_flags(short_flags, long_flags);
options.push(format_help_line(indent, width, &flags, help));
}
ArgType::Positional { .. } => {}
}
}
let help_flags = format_flags(short_help_flags, long_help_flags);
if !help_flags.is_empty() {
options.push(format_help_line(indent, width, &help_flags, "Display this help message"));
}
format!(
"Options:\n{}",
options.join("\n"),
)
}
fn format_flags(short: &[char], long: &[String]) -> String {
short.iter().map(|s| format!("-{s}"))
.chain(
long.iter().map(|l| format!("--{l}"))
).collect::<Vec<_>>()
.join(", ")
}
fn format_help_line(indent: usize, width: usize, flags: &str, help: &str) -> String {
let indent = " ".repeat(indent);
if help == "" {
format!("{indent}{flags}")
} else if flags.len() >= width {
let help_indent = " ".repeat(width);
format!("{indent}{flags}\n{indent}{help_indent}{help}")
} else {
let help_indent = " ".repeat(width-flags.len());
format!("{indent}{flags}{help_indent}{help}")
}
}
+15 -45
View File
@@ -3,20 +3,24 @@ use std::ops::RangeInclusive;
use syn::{
parse::{Parse, ParseStream},
punctuated::Punctuated,
Attribute, ExprLit, ExprRange, Ident, Lit, LitInt, LitStr, RangeLimits, Token,
Attribute, Expr, ExprLit, ExprRange, Ident, Lit, LitInt, LitStr, RangeLimits, Token,
};
use crate::{Arg, Expr};
use crate::Arg;
#[derive(Default)]
pub(crate) struct FlagAttr {
pub(crate) flags: Vec<Arg>,
pub(crate) value: Option<syn::Expr>,
pub(crate) enum ArgAttr {
Option(OptionAttr),
Positional(PositionalAttr),
}
enum FlagAttrArg {
Arg(Arg),
Value(Expr),
pub(crate) fn parse_argument_attribute(attr: &Attribute) -> ArgAttr {
if attr.path.is_ident("option") {
ArgAttr::Option(parse_option_attr(attr))
} else if attr.path.is_ident("positional") {
ArgAttr::Positional(parse_positional_attr(attr))
} else {
panic!("Internal error: invalid argument attribute");
}
}
#[derive(Default)]
@@ -57,41 +61,7 @@ enum PositionalAttrArg {
NumArgs(RangeInclusive<usize>),
}
pub(crate) fn parse_flag_attr(attr: Attribute) -> FlagAttr {
let mut flag_attr = FlagAttr::default();
let Ok(parsed_args) = attr
.parse_args_with(Punctuated::<FlagAttrArg, Token![,]>::parse_terminated)
else {
return flag_attr;
};
for arg in parsed_args {
match arg {
FlagAttrArg::Arg(a) => flag_attr.flags.push(a),
FlagAttrArg::Value(e) => flag_attr.value = Some(e),
};
}
flag_attr
}
impl Parse for FlagAttrArg {
fn parse(input: ParseStream) -> syn::Result<Self> {
if input.peek(LitStr) {
return parse_flag(input).map(Self::Arg);
}
if input.peek(Ident) {
let name = input.parse::<Ident>()?.to_string();
input.parse::<Token![=]>()?;
match name.as_str() {
"value" => return Ok(Self::Value(input.parse::<Expr>()?)),
_ => panic!("Unrecognized argument {} for flag attribute", name),
};
}
panic!("Arguments to flag attribute must be string literals");
}
}
pub(crate) fn parse_option_attr(attr: Attribute) -> OptionAttr {
pub(crate) fn parse_option_attr(attr: &Attribute) -> OptionAttr {
let mut option_attr = OptionAttr::default();
let Ok(parsed_args) = attr
.parse_args_with(Punctuated::<OptionAttrArg, Token![,]>::parse_terminated)
@@ -177,7 +147,7 @@ fn parse_flag(input: ParseStream) -> syn::Result<Arg> {
panic!("Arguments to flag must start with \"-\" or \"--\"");
}
pub(crate) fn parse_positional_attr(attr: Attribute) -> PositionalAttr {
pub(crate) fn parse_positional_attr(attr: &Attribute) -> PositionalAttr {
let mut positional_attr = PositionalAttr::default();
let Ok(parsed_args) = attr
.parse_args_with(Punctuated::<PositionalAttrArg, Token![,]>::parse_terminated)
+42 -215
View File
@@ -1,19 +1,21 @@
mod action;
mod argument;
mod attributes;
use action::{parse_action_attr, ActionAttr, ActionType};
use attributes::{
parse_flag_attr, parse_option_attr, parse_positional_attr, parse_value_attr, FlagAttr,
OptionAttr, PositionalAttr, ValueAttr,
use argument::{
help_handling, long_handling, parse_argument, parse_help_flags, positional_handling,
short_handling,
};
use attributes::{parse_value_attr, ValueAttr};
use proc_macro::TokenStream;
use quote::quote;
use syn::{
parse::Parse,
parse_macro_input, Attribute,
parse_macro_input,
Data::{Enum, Struct},
DeriveInput, Expr, Fields,
DeriveInput, Fields,
};
#[derive(Eq, Hash, PartialEq, Debug, Clone)]
@@ -22,12 +24,6 @@ enum Arg {
Long(String),
}
enum DeriveAttribute {
Flag(FlagAttr),
Option(OptionAttr),
Positional(PositionalAttr),
}
#[proc_macro_derive(Options, attributes(arg_type, map, set, set_true, set_false, collect))]
pub fn options(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
@@ -112,11 +108,18 @@ pub fn options(input: TokenStream) -> TokenStream {
I: IntoIterator + 'static,
I::Item: Into<std::ffi::OsString>,
{
use uutils_args::lexopt;
use uutils_args::FromValue;
use uutils_args::{lexopt, FromValue, Argument};
let mut iter = #arg_type::parse(args);
while let Some(arg) = iter.next_arg()? {
#(#stmts)*
match arg {
Argument::Help => {
println!("{}", #arg_type::help());
std::process::exit(0);
},
Argument::Custom(arg) => {
#(#stmts)*
}
}
}
#arg_type::check_missing(iter.positional_idx)?;
Ok(())
@@ -138,184 +141,42 @@ pub fn arguments(input: TokenStream) -> TokenStream {
panic!("Input should be an enum!");
};
let mut short_match_arms = Vec::new();
let mut long_match_arms = Vec::new();
let mut long_options = Vec::new();
let (short_help_flags, long_help_flags) = parse_help_flags(&input.attrs);
let arguments: Vec<_> = data.variants.into_iter().flat_map(parse_argument).collect();
let mut positional_indices = Vec::new();
for variant in data.variants {
let variant_ident = variant.ident;
let variant_name = variant_ident.to_string();
let mut short_flags = Vec::new();
let mut long_flags = Vec::new();
let parse_expr = match variant.fields {
Fields::Unit => {
for attr in variant.attrs {
let Some(attr) = parse_attr(attr) else { continue; };
let DeriveAttribute::Flag(f) = attr else {
panic!("unsupported attribute");
};
let (mut shorts, mut longs) = flag_names(f.flags, &variant_name);
short_flags.append(&mut shorts);
long_flags.append(&mut longs);
}
quote!(Self::#variant_ident)
}
Fields::Unnamed(f) => {
let v: Vec<_> = f.unnamed.iter().collect();
assert_eq!(v.len(), 1, "Options can have only one field");
let field_type = v[0].ty.clone();
let field_is_option = {
if let syn::Type::Path(field_type_path) = field_type {
field_type_path
.path
.segments
.iter()
.map(|s| s.ident.to_string())
.collect::<Vec<_>>()
== vec!["Option"]
} else {
false
}
};
for attr in variant.attrs {
let Some(attr) = parse_attr(attr) else { continue; };
match attr {
DeriveAttribute::Option(f) => {
let (mut shorts, mut longs) = flag_names(f.flags, &variant_name);
short_flags.append(&mut shorts);
long_flags.append(&mut longs);
}
DeriveAttribute::Positional(p) => {
positional_indices.push((
p.num_args,
variant_name.to_uppercase(),
quote!(Self::#variant_ident (FromValue::from_value(value)?)),
));
}
_ => panic!("unsupported attribute"),
};
}
if field_is_option {
quote!(Self::#variant_ident (match parser.optional_value() {
Some(v) => Some(FromValue::from_value(v)?),
None => None,
}))
} else {
quote!(Self::#variant_ident (FromValue::from_value(parser.value()?)?))
}
}
_ => panic!("unimplemented"),
};
if !short_flags.is_empty() {
short_match_arms.push(quote!(#(#short_flags)|* => { #parse_expr }));
};
if !long_flags.is_empty() {
long_match_arms.push(quote!(#(#long_flags)|* => { #parse_expr }));
long_options.append(&mut long_flags);
};
}
let long_handling = if long_options.is_empty() {
quote!(return Err(arg.unexpected().into()))
} else {
let long_opt_len = long_options.len();
quote!(
let long_options: [&str; #long_opt_len] = [#(#long_options),*];
let mut candidates = Vec::new();
let mut exact_match = None;
for opt in long_options {
if opt == long {
exact_match = Some(opt);
break;
} else if opt.starts_with(long) {
candidates.push(opt);
}
}
let opt = match (exact_match, &candidates[..]) {
(Some(opt), _) => opt,
(None, [opt]) => opt,
(None, []) => return Err(arg.unexpected().into()),
(None, opts) => return Err(Error::AmbiguousOption {
option: long.to_string(),
candidates: candidates.iter().map(|s| s.to_string()).collect(),
})
};
match opt {
#(#long_match_arms)*
_ => unreachable!("Should be caught by (None, []) case above.")
}
)
};
let mut positional_match_arms = Vec::new();
// The largest index of the previous argument, so the the argument after this should
// belong to the next argument.
let mut last_index = 0;
// The minimum number of arguments needed to not return a missing argument error.
let mut minimum_needed = 0;
let mut missing_argument_checks = vec![];
for (range, arg_name, expr) in positional_indices {
if *range.start() > 0 {
minimum_needed = last_index + range.start();
missing_argument_checks.push(quote!(if positional_idx < #minimum_needed {
missing.push(#arg_name);
}));
}
last_index += range.end();
positional_match_arms.push(quote!(0..=#last_index => { #expr }));
}
let short = short_handling(&arguments);
let long = long_handling(&arguments);
let (positional, missing_argument_checks) = positional_handling(&arguments);
let help = help_handling(&arguments, &short_help_flags, &long_help_flags);
let expanded = quote!(
impl #impl_generics Arguments for #name #ty_generics #where_clause {
#[allow(unreachable_code)]
fn next_arg(parser: &mut uutils_args::lexopt::Parser, positional_idx: &mut usize) -> Result<Option<Self>, uutils_args::Error> {
use uutils_args::FromValue;
use uutils_args::lexopt::Arg;
use uutils_args::Error;
fn next_arg(
parser: &mut uutils_args::lexopt::Parser, positional_idx: &mut usize
) -> Result<Option<uutils_args::Argument<Self>>, uutils_args::Error> {
use uutils_args::{FromValue, lexopt, Error, Argument};
let Some(arg) = parser.next()? else { return Ok(None); };
if let lexopt::Arg::Short('h') | lexopt::Arg::Long("help") = arg {
return Ok(Some(Argument::Help));
}
let parsed = match arg {
Arg::Short(short) => {
match short {
#(#short_match_arms)*
_ => return Err(arg.unexpected().into())
}
}
Arg::Long(long) => {
#long_handling
}
Arg::Value(value) => {
*positional_idx += 1;
match positional_idx {
#(#positional_match_arms)*
_ => return Err(Arg::Value(value).unexpected().into()),
}
}
lexopt::Arg::Short(short) => { #short }
lexopt::Arg::Long(long) => { #long }
lexopt::Arg::Value(value) => { #positional }
};
Ok(Some(parsed))
Ok(Some(Argument::Custom(parsed)))
}
fn check_missing(positional_idx: usize) -> Result<(), uutils_args::Error> {
// We have the minimum number of required arguments overall.
// So we don't need to check the others.
if positional_idx >= #minimum_needed {
return Ok(());
}
let mut missing: Vec<&str> = vec![];
#(#missing_argument_checks)*
if !missing.is_empty() {
Err(uutils_args::Error::MissingPositionalArguments(
missing.iter().map(ToString::to_string).collect::<Vec<String>>()
))
} else {
Ok(())
}
#missing_argument_checks
}
fn help() -> &'static str {
#help
}
}
);
@@ -382,37 +243,3 @@ pub fn from_value(input: TokenStream) -> TokenStream {
TokenStream::from(expanded)
}
fn flag_names(flags: Vec<Arg>, field_name: &str) -> (Vec<char>, Vec<String>) {
let field_name = field_name.to_lowercase();
if flags.is_empty() {
let first_char = field_name.chars().next().unwrap();
if field_name.len() > 1 {
(vec![first_char], vec![field_name.to_string()])
} else {
(vec![first_char], vec![])
}
} else {
let mut shorts = Vec::new();
let mut longs = Vec::new();
for flag in flags {
match flag {
Arg::Short(x) => shorts.push(x),
Arg::Long(x) => longs.push(x),
};
}
(shorts, longs)
}
}
fn parse_attr(attr: Attribute) -> Option<DeriveAttribute> {
if attr.path.is_ident("flag") {
Some(DeriveAttribute::Flag(parse_flag_attr(attr)))
} else if attr.path.is_ident("option") {
Some(DeriveAttribute::Option(parse_option_attr(attr)))
} else if attr.path.is_ident("positional") {
Some(DeriveAttribute::Positional(parse_positional_attr(attr)))
} else {
None
}
}
+28
View File
@@ -0,0 +1,28 @@
use uutils_args::{Arguments, Options};
#[derive(Clone, Arguments)]
enum Arg {
/// The name to greet
#[option]
Name(String),
/// The number of times to greet
#[option]
Count(u8),
}
#[derive(Default, Options)]
#[arg_type(Arg)]
struct Settings {
#[set(Arg::Name)]
name: String,
#[set(Arg::Count)]
count: u8,
}
fn main() {
let settings = Settings::parse(std::env::args_os()).unwrap();
for _ in 0..settings.count {
println!("Hello, {}!", settings.name);
}
}
+11 -3
View File
@@ -45,6 +45,12 @@ impl From<lexopt::Error> for Error {
}
}
#[derive(Clone)]
pub enum Argument<T: Arguments> {
Help,
Custom(T),
}
pub trait Arguments: Sized + Clone {
fn parse<I>(args: I) -> ArgumentIter<Self>
where
@@ -57,9 +63,11 @@ pub trait Arguments: Sized + Clone {
fn next_arg(
parser: &mut lexopt::Parser,
positional_idx: &mut usize,
) -> Result<Option<Self>, Error>;
) -> Result<Option<Argument<Self>>, Error>;
fn check_missing(positional_idx: usize) -> Result<(), Error>;
fn help() -> &'static str;
}
pub struct ArgumentIter<T: Arguments> {
@@ -75,13 +83,13 @@ impl<T: Arguments> ArgumentIter<T> {
I::Item: Into<OsString>,
{
Self {
parser: lexopt::Parser::from_args(args),
parser: lexopt::Parser::from_iter(args),
positional_idx: 0,
t: PhantomData,
}
}
pub fn next_arg(&mut self) -> Result<Option<T>, Error> {
pub fn next_arg(&mut self) -> Result<Option<Argument<T>>, Error> {
T::next_arg(&mut self.parser, &mut self.positional_idx)
}
}
+104 -65
View File
@@ -1,8 +1,8 @@
use uutils_args::{ArgumentIter, Arguments, Options};
use uutils_args::{Argument, ArgumentIter, Arguments, Options};
fn to_vec<T: Arguments>(mut args: ArgumentIter<T>) -> Vec<T> {
let mut v = Vec::new();
while let Some(arg) = args.next_arg().unwrap() {
while let Some(Argument::Custom(arg)) = args.next_arg().unwrap() {
v.push(arg);
}
v
@@ -12,7 +12,7 @@ fn to_vec<T: Arguments>(mut args: ArgumentIter<T>) -> Vec<T> {
fn one_flag() {
#[derive(Arguments, Clone, Debug, PartialEq, Eq)]
enum Arg {
#[flag]
#[option]
Foo,
}
@@ -23,10 +23,10 @@ fn one_flag() {
foo: bool,
}
let iter = Arg::parse(["-f", "--foo"]);
let iter = Arg::parse(["test", "-f", "--foo"]);
assert_eq!(to_vec(iter), vec![Arg::Foo, Arg::Foo]);
let settings = Settings::parse(["-f"]).unwrap();
let settings = Settings::parse(["test", "-f"]).unwrap();
assert!(settings.foo);
}
@@ -34,9 +34,9 @@ fn one_flag() {
fn two_flags() {
#[derive(Arguments, Clone)]
enum Arg {
#[flag]
#[option]
A,
#[flag]
#[option]
B,
}
@@ -50,7 +50,7 @@ fn two_flags() {
}
assert_eq!(
Settings::parse(["-a"]).unwrap(),
Settings::parse(["test", "-a"]).unwrap(),
Settings { a: true, b: false }
);
assert_eq!(
@@ -58,11 +58,11 @@ fn two_flags() {
Settings { a: false, b: false }
);
assert_eq!(
Settings::parse(["-b"]).unwrap(),
Settings::parse(["test", "-b"]).unwrap(),
Settings { a: false, b: true }
);
assert_eq!(
Settings::parse(["-a", "-b"]).unwrap(),
Settings::parse(["test", "-a", "-b"]).unwrap(),
Settings { a: true, b: true }
);
}
@@ -71,7 +71,7 @@ fn two_flags() {
fn long_and_short_flag() {
#[derive(Arguments, Clone)]
enum Arg {
#[flag]
#[option]
Foo,
}
@@ -86,15 +86,21 @@ fn long_and_short_flag() {
Settings::parse::<&[&std::ffi::OsStr]>(&[]).unwrap(),
Settings { foo: false },
);
assert_eq!(Settings::parse(["--foo"]).unwrap(), Settings { foo: true },);
assert_eq!(Settings::parse(["-f"]).unwrap(), Settings { foo: true },);
assert_eq!(
Settings::parse(["test", "--foo"]).unwrap(),
Settings { foo: true },
);
assert_eq!(
Settings::parse(["test", "-f"]).unwrap(),
Settings { foo: true },
);
}
#[test]
fn short_alias() {
#[derive(Arguments, Clone)]
enum Arg {
#[flag("-b")]
#[option("-b")]
Foo,
}
@@ -105,14 +111,17 @@ fn short_alias() {
foo: bool,
}
assert_eq!(Settings::parse(["-b"]).unwrap(), Settings { foo: true },);
assert_eq!(
Settings::parse(["test", "-b"]).unwrap(),
Settings { foo: true },
);
}
#[test]
fn long_alias() {
#[derive(Arguments, Clone)]
enum Arg {
#[flag("--bar")]
#[option("--bar")]
Foo,
}
@@ -123,16 +132,19 @@ fn long_alias() {
foo: bool,
}
assert_eq!(Settings::parse(["--bar"]).unwrap(), Settings { foo: true },);
assert_eq!(
Settings::parse(["test", "--bar"]).unwrap(),
Settings { foo: true },
);
}
#[test]
fn short_and_long_alias() {
#[derive(Arguments, Clone)]
enum Arg {
#[flag("-b", "--bar")]
#[option("-b", "--bar")]
Foo,
#[flag("-f", "--foo")]
#[option("-f", "--foo")]
Bar,
}
@@ -155,21 +167,21 @@ fn short_and_long_alias() {
bar: true,
};
assert_eq!(Settings::parse(["--bar"]).unwrap(), foo_true);
assert_eq!(Settings::parse(["-b"]).unwrap(), foo_true);
assert_eq!(Settings::parse(["--foo"]).unwrap(), bar_true);
assert_eq!(Settings::parse(["-f"]).unwrap(), bar_true);
assert_eq!(Settings::parse(["test", "--bar"]).unwrap(), foo_true);
assert_eq!(Settings::parse(["test", "-b"]).unwrap(), foo_true);
assert_eq!(Settings::parse(["test", "--foo"]).unwrap(), bar_true);
assert_eq!(Settings::parse(["test", "-f"]).unwrap(), bar_true);
}
#[test]
fn xyz_map_to_abc() {
#[derive(Arguments, Clone)]
enum Arg {
#[flag]
#[option]
X,
#[flag]
#[option]
Y,
#[flag]
#[option]
Z,
}
@@ -185,7 +197,7 @@ fn xyz_map_to_abc() {
}
assert_eq!(
Settings::parse(["-x"]).unwrap(),
Settings::parse(["test", "-x"]).unwrap(),
Settings {
a: true,
b: true,
@@ -194,7 +206,7 @@ fn xyz_map_to_abc() {
);
assert_eq!(
Settings::parse(["-y"]).unwrap(),
Settings::parse(["test", "-y"]).unwrap(),
Settings {
a: false,
b: true,
@@ -203,7 +215,7 @@ fn xyz_map_to_abc() {
);
assert_eq!(
Settings::parse(["-xy"]).unwrap(),
Settings::parse(["test", "-xy"]).unwrap(),
Settings {
a: true,
b: true,
@@ -212,7 +224,7 @@ fn xyz_map_to_abc() {
);
assert_eq!(
Settings::parse(["-z"]).unwrap(),
Settings::parse(["test", "-z"]).unwrap(),
Settings {
a: true,
b: true,
@@ -225,9 +237,9 @@ fn xyz_map_to_abc() {
fn non_rust_ident() {
#[derive(Arguments, Clone)]
enum Arg {
#[flag("--foo-bar")]
#[option("--foo-bar")]
FooBar,
#[flag("--super")]
#[option("--super")]
Super,
}
@@ -241,7 +253,7 @@ fn non_rust_ident() {
}
assert_eq!(
Settings::parse(["--foo-bar", "--super"]).unwrap(),
Settings::parse(["test", "--foo-bar", "--super"]).unwrap(),
Settings { a: true, b: true }
)
}
@@ -250,7 +262,7 @@ fn non_rust_ident() {
fn number_flag() {
#[derive(Arguments, Clone)]
enum Arg {
#[flag("-1")]
#[option("-1")]
One,
}
#[derive(Default, Options, PartialEq, Eq, Debug)]
@@ -260,16 +272,19 @@ fn number_flag() {
one: bool,
}
assert_eq!(Settings::parse(["-1"]).unwrap(), Settings { one: true })
assert_eq!(
Settings::parse(["test", "-1"]).unwrap(),
Settings { one: true }
)
}
#[test]
fn false_bool() {
#[derive(Arguments, Clone)]
enum Arg {
#[flag]
#[option]
A,
#[flag]
#[option]
B,
}
@@ -283,16 +298,28 @@ fn false_bool() {
foo: bool,
}
assert_eq!(Settings::parse(["-a"]).unwrap(), Settings { foo: true });
assert_eq!(Settings::parse(["-b"]).unwrap(), Settings { foo: false });
assert_eq!(Settings::parse(["-ab"]).unwrap(), Settings { foo: false });
assert_eq!(Settings::parse(["-ba"]).unwrap(), Settings { foo: true });
assert_eq!(
Settings::parse(["-a", "-b"]).unwrap(),
Settings::parse(["test", "-a"]).unwrap(),
Settings { foo: true }
);
assert_eq!(
Settings::parse(["test", "-b"]).unwrap(),
Settings { foo: false }
);
assert_eq!(
Settings::parse(["-b", "-a"]).unwrap(),
Settings::parse(["test", "-ab"]).unwrap(),
Settings { foo: false }
);
assert_eq!(
Settings::parse(["test", "-ba"]).unwrap(),
Settings { foo: true }
);
assert_eq!(
Settings::parse(["test", "-a", "-b"]).unwrap(),
Settings { foo: false }
);
assert_eq!(
Settings::parse(["test", "-b", "-a"]).unwrap(),
Settings { foo: true }
);
@@ -304,16 +331,28 @@ fn false_bool() {
foo: bool,
}
assert_eq!(Settings2::parse(["-a"]).unwrap(), Settings2 { foo: true });
assert_eq!(Settings2::parse(["-b"]).unwrap(), Settings2 { foo: false });
assert_eq!(Settings2::parse(["-ab"]).unwrap(), Settings2 { foo: false });
assert_eq!(Settings2::parse(["-ba"]).unwrap(), Settings2 { foo: true });
assert_eq!(
Settings2::parse(["-a", "-b"]).unwrap(),
Settings2::parse(["test", "-a"]).unwrap(),
Settings2 { foo: true }
);
assert_eq!(
Settings2::parse(["test", "-b"]).unwrap(),
Settings2 { foo: false }
);
assert_eq!(
Settings2::parse(["-b", "-a"]).unwrap(),
Settings2::parse(["test", "-ab"]).unwrap(),
Settings2 { foo: false }
);
assert_eq!(
Settings2::parse(["test", "-ba"]).unwrap(),
Settings2 { foo: true }
);
assert_eq!(
Settings2::parse(["test", "-a", "-b"]).unwrap(),
Settings2 { foo: false }
);
assert_eq!(
Settings2::parse(["test", "-b", "-a"]).unwrap(),
Settings2 { foo: true }
);
}
@@ -330,11 +369,11 @@ fn enum_flag() {
#[derive(Arguments, Clone)]
enum Arg {
#[flag]
#[option]
Foo,
#[flag]
#[option]
Bar,
#[flag]
#[option]
Baz,
}
@@ -355,12 +394,12 @@ fn enum_flag() {
);
assert_eq!(
Settings::parse(["--bar"]).unwrap().foo,
Settings::parse(["test", "--bar"]).unwrap().foo,
SomeEnum::VariantBar
);
assert_eq!(
Settings::parse(["--baz"]).unwrap().foo,
Settings::parse(["test", "--baz"]).unwrap().foo,
SomeEnum::VariantBaz,
);
}
@@ -369,7 +408,7 @@ fn enum_flag() {
fn count() {
#[derive(Arguments, Clone)]
enum Arg {
#[flag]
#[option]
Verbosity,
}
@@ -380,20 +419,20 @@ fn count() {
verbosity: u8,
}
assert_eq!(Settings::parse(["-v"]).unwrap().verbosity, 1);
assert_eq!(Settings::parse(["-vv"]).unwrap().verbosity, 2);
assert_eq!(Settings::parse(["-vvv"]).unwrap().verbosity, 3);
assert_eq!(Settings::parse(["test", "-v"]).unwrap().verbosity, 1);
assert_eq!(Settings::parse(["test", "-vv"]).unwrap().verbosity, 2);
assert_eq!(Settings::parse(["test", "-vvv"]).unwrap().verbosity, 3);
}
#[test]
fn infer_long_args() {
#[derive(Arguments, Clone)]
enum Arg {
#[flag("--all")]
#[option("--all")]
All,
#[flag("--almost-all")]
#[option("--almost-all")]
AlmostAll,
#[flag("--author")]
#[option("--author")]
Author,
}
@@ -408,8 +447,8 @@ fn infer_long_args() {
author: bool,
}
assert!(Settings::parse(["--all"]).unwrap().all);
assert!(Settings::parse(["--alm"]).unwrap().almost_all);
assert!(Settings::parse(["--au"]).unwrap().author);
assert!(Settings::parse(["--a"]).is_err());
assert!(Settings::parse(["test", "--all"]).unwrap().all);
assert!(Settings::parse(["test", "--alm"]).unwrap().almost_all);
assert!(Settings::parse(["test", "--au"]).unwrap().author);
assert!(Settings::parse(["test", "--a"]).is_err());
}
+43 -29
View File
@@ -16,7 +16,9 @@ fn string_option() {
}
assert_eq!(
Settings::parse(["--message=hello"]).unwrap().message,
Settings::parse(["test", "--message=hello"])
.unwrap()
.message,
"hello"
);
}
@@ -48,12 +50,12 @@ fn enum_option() {
}
assert_eq!(
Settings::parse(["--format=bar"]).unwrap().format,
Settings::parse(["test", "--format=bar"]).unwrap().format,
Format::Bar
);
assert_eq!(
Settings::parse(["--format", "baz"]).unwrap().format,
Settings::parse(["test", "--format", "baz"]).unwrap().format,
Format::Baz
);
}
@@ -83,11 +85,11 @@ fn enum_option_with_fields() {
}
assert_eq!(
Settings::parse(["-i=thin"]).unwrap().indent,
Settings::parse(["test", "-i=thin"]).unwrap().indent,
Indent::Spaces(4)
);
assert_eq!(
Settings::parse(["-i=wide"]).unwrap().indent,
Settings::parse(["test", "-i=wide"]).unwrap().indent,
Indent::Spaces(8)
);
}
@@ -130,8 +132,14 @@ fn enum_with_complex_from_value() {
indent: Indent,
}
assert_eq!(Settings::parse(["-i=tabs"]).unwrap().indent, Indent::Tabs);
assert_eq!(Settings::parse(["-i=4"]).unwrap().indent, Indent::Spaces(4));
assert_eq!(
Settings::parse(["test", "-i=tabs"]).unwrap().indent,
Indent::Tabs
);
assert_eq!(
Settings::parse(["test", "-i=4"]).unwrap().indent,
Indent::Spaces(4)
);
}
#[test]
@@ -164,23 +172,29 @@ fn color() {
}
assert_eq!(
Settings::parse(["--color=yes"]).unwrap().color,
Settings::parse(["test", "--color=yes"]).unwrap().color,
Color::Always
);
assert_eq!(
Settings::parse(["--color=always"]).unwrap().color,
Settings::parse(["test", "--color=always"]).unwrap().color,
Color::Always
);
assert_eq!(Settings::parse(["--color=no"]).unwrap().color, Color::Never);
assert_eq!(
Settings::parse(["--color=never"]).unwrap().color,
Settings::parse(["test", "--color=no"]).unwrap().color,
Color::Never
);
assert_eq!(
Settings::parse(["--color=auto"]).unwrap().color,
Settings::parse(["test", "--color=never"]).unwrap().color,
Color::Never
);
assert_eq!(
Settings::parse(["test", "--color=auto"]).unwrap().color,
Color::Auto
);
assert_eq!(Settings::parse(["--color"]).unwrap().color, Color::Always)
assert_eq!(
Settings::parse(["test", "--color"]).unwrap().color,
Color::Always
)
}
#[test]
@@ -189,9 +203,9 @@ fn actions() {
enum Arg {
#[option]
Message(String),
#[flag]
#[option]
Send,
#[flag]
#[option]
Receive,
}
@@ -213,7 +227,7 @@ fn actions() {
messages: Vec<String>,
}
let settings = Settings::parse(["-m=Hello", "-m=World", "--send"]).unwrap();
let settings = Settings::parse(["test", "-m=Hello", "-m=World", "--send"]).unwrap();
assert_eq!(settings.messages, vec!["Hello", "World"]);
assert_eq!(settings.message1, "World");
assert_eq!(settings.message2, "World");
@@ -235,11 +249,11 @@ fn width() {
Arg::Width(0) => None,
Arg::Width(x) => Some(x),
)]
width: Option<u64>
width: Option<u64>,
}
assert_eq!(Settings::parse(["-w=0"]).unwrap().width, None);
assert_eq!(Settings::parse(["-w=1"]).unwrap().width, Some(1));
assert_eq!(Settings::parse(["test", "-w=0"]).unwrap().width, None);
assert_eq!(Settings::parse(["test", "-w=1"]).unwrap().width, Some(1));
}
#[test]
@@ -286,15 +300,15 @@ fn integers() {
n: i128,
}
assert_eq!(Settings::parse(["--u8=5"]).unwrap().n, 5);
assert_eq!(Settings::parse(["--u16=5"]).unwrap().n, 5);
assert_eq!(Settings::parse(["--u32=5"]).unwrap().n, 5);
assert_eq!(Settings::parse(["--u64=5"]).unwrap().n, 5);
assert_eq!(Settings::parse(["--u128=5"]).unwrap().n, 5);
assert_eq!(Settings::parse(["test", "--u8=5"]).unwrap().n, 5);
assert_eq!(Settings::parse(["test", "--u16=5"]).unwrap().n, 5);
assert_eq!(Settings::parse(["test", "--u32=5"]).unwrap().n, 5);
assert_eq!(Settings::parse(["test", "--u64=5"]).unwrap().n, 5);
assert_eq!(Settings::parse(["test", "--u128=5"]).unwrap().n, 5);
assert_eq!(Settings::parse(["--i8=5"]).unwrap().n, 5);
assert_eq!(Settings::parse(["--i16=5"]).unwrap().n, 5);
assert_eq!(Settings::parse(["--i32=5"]).unwrap().n, 5);
assert_eq!(Settings::parse(["--i64=5"]).unwrap().n, 5);
assert_eq!(Settings::parse(["--i128=5"]).unwrap().n, 5);
assert_eq!(Settings::parse(["test", "--i8=5"]).unwrap().n, 5);
assert_eq!(Settings::parse(["test", "--i16=5"]).unwrap().n, 5);
assert_eq!(Settings::parse(["test", "--i32=5"]).unwrap().n, 5);
assert_eq!(Settings::parse(["test", "--i64=5"]).unwrap().n, 5);
assert_eq!(Settings::parse(["test", "--i128=5"]).unwrap().n, 5);
}
+93
View File
@@ -0,0 +1,93 @@
use std::ffi::OsString;
use uutils_args::{Arguments, Options};
const EMPTY: [OsString; 0] = [];
#[test]
fn one_positional() {
#[derive(Arguments, Clone)]
enum Arg {
#[positional(1)]
File1(String),
}
#[derive(Default, Options)]
#[arg_type(Arg)]
struct Settings {
#[set(Arg::File1)]
file1: String,
}
let settings = Settings::parse(["test", "foo"]).unwrap();
assert_eq!(settings.file1, "foo");
assert!(Settings::parse(EMPTY).is_err());
}
#[test]
fn two_positionals() {
#[derive(Arguments, Clone)]
enum Arg {
#[positional(1)]
Foo(String),
#[positional(1)]
Bar(String),
}
#[derive(Default, Options)]
#[arg_type(Arg)]
struct Settings {
#[set(Arg::Foo)]
foo: String,
#[set(Arg::Bar)]
bar: String,
}
let settings = Settings::parse(["test", "a", "b"]).unwrap();
assert_eq!(settings.foo, "a");
assert_eq!(settings.bar, "b");
assert!(Settings::parse(EMPTY).is_err());
}
#[test]
fn optional_positional() {
#[derive(Arguments, Clone)]
enum Arg {
#[positional(0..=1)]
Foo(String),
}
#[derive(Default, Options)]
#[arg_type(Arg)]
struct Settings {
#[map(Arg::Foo(s) => Some(s))]
foo: Option<String>,
}
let settings = Settings::parse(EMPTY).unwrap();
assert_eq!(settings.foo, None);
let settings = Settings::parse(["test", "bar"]).unwrap();
assert_eq!(settings.foo.unwrap(), "bar");
}
#[test]
fn collect_positional() {
#[derive(Arguments, Clone)]
enum Arg {
#[positional(..)]
Foo(String),
}
#[derive(Default, Options)]
#[arg_type(Arg)]
struct Settings {
#[collect(set(Arg::Foo))]
foo: Vec<String>,
}
let settings = Settings::parse(["test", "a", "b", "c"]).unwrap();
assert_eq!(settings.foo, vec!["a", "b", "c"]);
let settings = Settings::parse(EMPTY).unwrap();
assert_eq!(settings.foo, Vec::<String>::new());
}