diff --git a/derive/src/argument.rs b/derive/src/argument.rs index 453c285..34d0e94 100644 --- a/derive/src/argument.rs +++ b/derive/src/argument.rs @@ -6,7 +6,7 @@ use syn::{Attribute, Fields, FieldsUnnamed, Lit, Meta, Variant, Ident, punctuate use crate::{ attributes::{parse_argument_attribute, ArgAttr}, - Arg, + flags::Flags, }; pub(crate) struct Argument { @@ -24,8 +24,7 @@ pub(crate) enum TakesValue { pub(crate) enum ArgType { Option { - short_flags: Vec, - long_flags: Vec, + flags: Flags, takes_value: TakesValue, }, Positional { @@ -33,24 +32,32 @@ pub(crate) enum ArgType { }, } -pub(crate) fn parse_help_flags(attrs: &[Attribute]) -> (Vec, Vec) { +pub(crate) fn parse_help_flags(attrs: &[Attribute]) -> Flags { for attr in attrs { if attr.path.is_ident("help") { - let mut short = Vec::new(); - let mut long = Vec::new(); + let mut flags = Flags::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); + flags.add(&s); } + return flags; } } - return (vec!['h'], vec!["help".into()]) + return Flags::default_help(); +} + +pub(crate) fn parse_version_flags(attrs: &[Attribute]) -> Flags { + for attr in attrs { + if attr.path.is_ident("version") { + let mut flags = Flags::new(); + for s in attr.parse_args_with(Punctuated::::parse_terminated).unwrap() { + let s = s.value().to_string(); + flags.add(&s); + } + return flags; + } + } + return Flags::default_version(); } pub(crate) fn parse_argument(v: Variant) -> Option { @@ -76,15 +83,14 @@ pub(crate) fn parse_argument(v: Variant) -> Option { let arg_type = match attribute { ArgAttr::Option(opt) => { - let (short_flags, long_flags) = flag_names(opt.flags, &name); + let flags = opt.flags.or_from_name(&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, + flags, takes_value, } } @@ -144,42 +150,21 @@ fn type_is_option(syn_type: &syn::Type) -> bool { } } -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 { + let ArgType::Option { ref flags, .. } = arg.arg_type else { continue; }; - if short_flags.is_empty() { + if flags.short.is_empty() { continue; } - + + let pat = flags.short_pat(); let expr = argument_expression(arg); - match_arms.push(quote!(#(#short_flags)|* => { #expr })) + match_arms.push(quote!(#pat => { #expr })) } quote!( @@ -190,32 +175,34 @@ pub(crate) fn short_handling(args: &[Argument]) -> TokenStream { ) } -pub(crate) fn long_handling(args: &[Argument], long_help_flags: &[String]) -> TokenStream { +pub(crate) fn long_handling(args: &[Argument], help_flags: &Flags) -> TokenStream { let mut match_arms = Vec::new(); let mut options = Vec::new(); - options.extend(long_help_flags); + options.extend(help_flags.long.clone()); for arg in args { - let ArgType::Option { ref long_flags, .. } = arg.arg_type else { + let ArgType::Option { ref flags, .. } = arg.arg_type else { continue; }; - if long_flags.is_empty() { + if flags.long.is_empty() { continue; } + let pat = flags.long_pat(); let expr = argument_expression(arg); - match_arms.push(quote!(#(#long_flags)|* => { #expr })); - options.extend(long_flags); + match_arms.push(quote!(#pat => { #expr })); + options.extend(flags.long.clone()); } if options.is_empty() { return quote!(return Err(arg.unexpected().into())); } - let help_check = if !long_help_flags.is_empty() { - quote!(if let #(#long_help_flags)|* = opt { + let help_check = if !help_flags.long.is_empty() { + let pat = help_flags.long_pat(); + quote!(if let #pat = opt { return Ok(Some(Argument::Help)); }) } else { @@ -334,13 +321,12 @@ fn argument_expression(arg: &Argument) -> TokenStream { } } -pub(crate) fn help_handling(short_help_flags: &[char], long_help_flags: &[String]) -> TokenStream { - let pat = match (short_help_flags, long_help_flags) { - ([], []) => return quote!(), - (short, []) => quote!(lexopt::Arg::Short(#(#short)|*)), - ([], long) => quote!(lexopt::Arg::Long(#(#long)|*)), - (short, long) => quote!(lexopt::Arg::Short(#(#short)|*) | lexopt::Arg::Long(#(#long)|*)) - }; +pub(crate) fn help_handling(help_flags: &Flags) -> TokenStream { + if help_flags.is_empty() { + return quote!(); + } + + let pat = help_flags.pat(); quote!( if let #pat = arg { @@ -349,7 +335,21 @@ pub(crate) fn help_handling(short_help_flags: &[char], long_help_flags: &[String ) } -pub(crate) fn help_string(args: &[Argument], short_help_flags: &[char], long_help_flags: &[String]) -> TokenStream { +pub(crate) fn version_handling(version_flags: &Flags) -> TokenStream { + if version_flags.is_empty() { + return quote!(); + } + + let pat = version_flags.pat(); + + quote!( + if let #pat = arg { + return Ok(Some(Argument::Version)); + } + ) +} + +pub(crate) fn help_string(args: &[Argument], help_flags: &Flags, version_flags: &Flags) -> TokenStream { let mut options = Vec::new(); let width = 16; @@ -357,19 +357,24 @@ pub(crate) fn help_string(args: &[Argument], short_help_flags: &[char], long_hel for Argument { arg_type, help, ..} in args { match arg_type { - ArgType::Option { short_flags, long_flags, ..} => { - let flags = format_flags(short_flags, long_flags); + ArgType::Option { flags, ..} => { + let flags = flags.format(); 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() { + let help_flags = help_flags.format(); options.push(format_help_line(indent, width, &help_flags, "Display this help message")); } + if !version_flags.is_empty() { + let version_flags = version_flags.format(); + options.push(format_help_line(indent, width, &version_flags, "Display version information")); + } + let options = format!( "Options:\n{}", options.join("\n"), @@ -380,14 +385,6 @@ pub(crate) fn help_string(args: &[Argument], short_help_flags: &[char], long_hel ) } -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 == "" { diff --git a/derive/src/attributes.rs b/derive/src/attributes.rs index 1267c32..3630610 100644 --- a/derive/src/attributes.rs +++ b/derive/src/attributes.rs @@ -6,7 +6,7 @@ use syn::{ Attribute, Expr, ExprLit, ExprRange, Ident, Lit, LitInt, LitStr, RangeLimits, Token, }; -use crate::Arg; +use crate::{flags::Flags, Arg}; pub(crate) enum ArgAttr { Option(OptionAttr), @@ -25,7 +25,7 @@ pub(crate) fn parse_argument_attribute(attr: &Attribute) -> ArgAttr { #[derive(Default)] pub(crate) struct OptionAttr { - pub(crate) flags: Vec, + pub(crate) flags: Flags, // This should probably not accept any expr to give better errors. // Closures should be allowed though. pub(crate) parser: Option, @@ -71,7 +71,8 @@ pub(crate) fn parse_option_attr(attr: &Attribute) -> OptionAttr { for arg in parsed_args { match arg { - OptionAttrArg::Arg(a) => option_attr.flags.push(a), + OptionAttrArg::Arg(Arg::Short(a)) => option_attr.flags.short.push(a), + OptionAttrArg::Arg(Arg::Long(a)) => option_attr.flags.long.push(a), OptionAttrArg::Parser(e) => option_attr.parser = Some(e), }; } @@ -217,6 +218,6 @@ impl Parse for PositionalAttrArg { // _ => panic!("Unrecognized argument {} for option attribute", name), // }; // } - panic!("unpexpected argument to positional"); + panic!("unexpected argument to positional"); } } diff --git a/derive/src/flags.rs b/derive/src/flags.rs new file mode 100644 index 0000000..be8ef59 --- /dev/null +++ b/derive/src/flags.rs @@ -0,0 +1,95 @@ +use proc_macro2::TokenStream; +use quote::quote; + +#[derive(Default)] +pub(crate) struct Flags { + pub short: Vec, + pub long: Vec, +} + +impl Flags { + pub(crate) fn new() -> Self { + Self { + short: Vec::new(), + long: Vec::new(), + } + } + + pub(crate) fn add(&mut self, flag: &str) { + assert!(flag.starts_with('-'), "Flags must start with a '-'"); + if let Some(s) = flag.strip_prefix("--") { + self.long.push(s.to_string()); + } else if let Some(s) = flag.strip_prefix("-") { + assert_eq!(s.len(), 1); + self.short.push(s.chars().next().unwrap()) + } + } + + pub(crate) fn is_empty(&self) -> bool { + self.short.is_empty() && self.long.is_empty() + } + + pub(crate) fn short_pat(&self) -> TokenStream { + let short = &self.short; + quote!(#(#short)|*) + } + + pub(crate) fn long_pat(&self) -> TokenStream { + let long = &self.long; + quote!(#(#long)|*) + } + + pub(crate) fn pat(&self) -> TokenStream { + match (&self.short[..], &self.long[..]) { + ([], []) => panic!("Creating pattern from empty flags, probably not what you want!"), + (short, []) => quote!(lexopt::Arg::Short(#(#short)|*)), + ([], long) => quote!(lexopt::Arg::Long(#(#long)|*)), + (short, long) => { + quote!(lexopt::Arg::Short(#(#short)|*) | lexopt::Arg::Long(#(#long)|*)) + } + } + } + + pub(crate) fn default_help() -> Self { + Self { + short: vec!['h'], + long: vec!["help".into()], + } + } + + pub(crate) fn default_version() -> Self { + Self { + short: vec!['V'], + long: vec!["version".into()], + } + } + + pub(crate) fn format(&self) -> String { + self.short + .iter() + .map(|s| format!("-{s}")) + .chain(self.long.iter().map(|l| format!("--{l}"))) + .collect::>() + .join(", ") + } + + pub(crate) fn or_from_name(self, name: &str) -> Self { + if self.is_empty() { + let name = name.to_lowercase(); + let first_char = name.chars().next().unwrap(); + if name.len() > 1 { + Self { + short: vec![first_char], + long: vec![name.to_string()], + } + } else { + Self { + short: vec![first_char], + long: vec![], + } + } + } else { + self + } + } +} diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 2472509..40e023c 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -1,11 +1,12 @@ mod action; mod argument; mod attributes; +mod flags; use action::{parse_action_attr, ActionAttr, ActionType}; use argument::{ help_handling, help_string, long_handling, parse_argument, parse_help_flags, - positional_handling, short_handling, + parse_version_flags, positional_handling, short_handling, version_handling, }; use attributes::{parse_value_attr, ValueAttr}; @@ -116,6 +117,9 @@ pub fn options(input: TokenStream) -> TokenStream { println!("{}", iter.help()); std::process::exit(0); }, + Argument::Version => { + println!("{}", iter.version()); + }, Argument::Custom(arg) => { #(#stmts)* } @@ -141,14 +145,21 @@ pub fn arguments(input: TokenStream) -> TokenStream { panic!("Input should be an enum!"); }; - let (short_help_flags, long_help_flags) = parse_help_flags(&input.attrs); + let help_flags = parse_help_flags(&input.attrs); + let version_flags = parse_version_flags(&input.attrs); let arguments: Vec<_> = data.variants.into_iter().flat_map(parse_argument).collect(); let short = short_handling(&arguments); - let long = long_handling(&arguments, &long_help_flags); + let long = long_handling(&arguments, &help_flags); let (positional, missing_argument_checks) = positional_handling(&arguments); - let help_string = help_string(&arguments, &short_help_flags, &long_help_flags); - let help = help_handling(&short_help_flags, &long_help_flags); + let help_string = help_string(&arguments, &help_flags, &version_flags); + let help = help_handling(&help_flags); + let version = version_handling(&version_flags); + let version_string = quote!(format!( + "{} {}", + option_env!("CARGO_BIN_NAME").unwrap_or(env!("CARGO_PKG_NAME")), + env!("CARGO_PKG_VERSION"), + )); let expanded = quote!( impl #impl_generics Arguments for #name #ty_generics #where_clause { @@ -162,6 +173,8 @@ pub fn arguments(input: TokenStream) -> TokenStream { #help + #version + let parsed = match arg { lexopt::Arg::Short(short) => { #short } lexopt::Arg::Long(long) => { #long } @@ -177,6 +190,10 @@ pub fn arguments(input: TokenStream) -> TokenStream { fn help(bin_name: &str) -> String { #help_string } + + fn version() -> String { + #version_string + } } ); diff --git a/src/lib.rs b/src/lib.rs index 6af0a3c..5f33db7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,6 +48,7 @@ impl From for Error { #[derive(Clone)] pub enum Argument { Help, + Version, Custom(T), } @@ -68,6 +69,8 @@ pub trait Arguments: Sized + Clone { fn check_missing(positional_idx: usize) -> Result<(), Error>; fn help(bin_name: &str) -> String; + + fn version() -> String; } pub struct ArgumentIter { @@ -96,6 +99,10 @@ impl ArgumentIter { pub fn help(&self) -> String { T::help(self.parser.bin_name().unwrap()) } + + pub fn version(&self) -> String { + T::version() + } } pub trait Options: Sized + Default {