diff --git a/derive/src/argument.rs b/derive/src/argument.rs new file mode 100644 index 0000000..273ef8c --- /dev/null +++ b/derive/src/argument.rs @@ -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, + long_flags: Vec, + takes_value: TakesValue, + }, + Positional { + num_args: RangeInclusive, + }, +} + +pub(crate) fn parse_help_flags(attrs: &[Attribute]) -> (Vec, Vec) { + 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::::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 { + 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 { + 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!["Option"] + } else { + false + } +} + +fn flag_names(flags: Vec, field_name: &str) -> (Vec, Vec) { + 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::>() + )) + } 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::>() + .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}") + } +} diff --git a/derive/src/attributes.rs b/derive/src/attributes.rs index df16d4e..1267c32 100644 --- a/derive/src/attributes.rs +++ b/derive/src/attributes.rs @@ -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, - pub(crate) value: Option, +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), } -pub(crate) fn parse_flag_attr(attr: Attribute) -> FlagAttr { - let mut flag_attr = FlagAttr::default(); - let Ok(parsed_args) = attr - .parse_args_with(Punctuated::::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 { - if input.peek(LitStr) { - return parse_flag(input).map(Self::Arg); - } - - if input.peek(Ident) { - let name = input.parse::()?.to_string(); - input.parse::()?; - match name.as_str() { - "value" => return Ok(Self::Value(input.parse::()?)), - _ => 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::::parse_terminated) @@ -177,7 +147,7 @@ fn parse_flag(input: ParseStream) -> syn::Result { 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::::parse_terminated) diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 6ed2bce..1589146 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -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, { - 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!["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, 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>, 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::>() - )) - } 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, field_name: &str) -> (Vec, Vec) { - 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 { - 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 - } -} diff --git a/examples/hello_world.rs b/examples/hello_world.rs new file mode 100644 index 0000000..aad17b6 --- /dev/null +++ b/examples/hello_world.rs @@ -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); + } +} diff --git a/src/lib.rs b/src/lib.rs index 6d482f6..cdc74e3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,6 +45,12 @@ impl From for Error { } } +#[derive(Clone)] +pub enum Argument { + Help, + Custom(T), +} + pub trait Arguments: Sized + Clone { fn parse(args: I) -> ArgumentIter where @@ -57,9 +63,11 @@ pub trait Arguments: Sized + Clone { fn next_arg( parser: &mut lexopt::Parser, positional_idx: &mut usize, - ) -> Result, Error>; + ) -> Result>, Error>; fn check_missing(positional_idx: usize) -> Result<(), Error>; + + fn help() -> &'static str; } pub struct ArgumentIter { @@ -75,13 +83,13 @@ impl ArgumentIter { I::Item: Into, { 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, Error> { + pub fn next_arg(&mut self) -> Result>, Error> { T::next_arg(&mut self.parser, &mut self.positional_idx) } } diff --git a/tests/flags.rs b/tests/flags.rs index 0f4f2f3..b94f594 100644 --- a/tests/flags.rs +++ b/tests/flags.rs @@ -1,8 +1,8 @@ -use uutils_args::{ArgumentIter, Arguments, Options}; +use uutils_args::{Argument, ArgumentIter, Arguments, Options}; fn to_vec(mut args: ArgumentIter) -> Vec { 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(mut args: ArgumentIter) -> Vec { 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()); } diff --git a/tests/options.rs b/tests/options.rs index e2d8bc3..27ee7dd 100644 --- a/tests/options.rs +++ b/tests/options.rs @@ -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, } - 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 + width: Option, } - 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); } diff --git a/tests/positionals.rs b/tests/positionals.rs new file mode 100644 index 0000000..3eb5f11 --- /dev/null +++ b/tests/positionals.rs @@ -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, + } + + 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, + } + + 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::::new()); +}