add version flags and refactor all flag handling with a Flags struct

This commit is contained in:
Terts Diepraam
2022-12-10 20:43:15 +01:00
parent 2e3171a604
commit 58bbbfe6f4
5 changed files with 196 additions and 79 deletions
+67 -70
View File
@@ -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<char>,
long_flags: Vec<String>,
flags: Flags,
takes_value: TakesValue,
},
Positional {
@@ -33,24 +32,32 @@ pub(crate) enum ArgType {
},
}
pub(crate) fn parse_help_flags(attrs: &[Attribute]) -> (Vec<char>, Vec<String>) {
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::<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);
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::<LitStr, Token![,]>::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<Argument> {
@@ -76,15 +83,14 @@ pub(crate) fn parse_argument(v: Variant) -> Option<Argument> {
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<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 {
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::<Vec<_>>()
.join(", ")
}
fn format_help_line(indent: usize, width: usize, flags: &str, help: &str) -> String {
let indent = " ".repeat(indent);
if help == "" {
+5 -4
View File
@@ -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<Arg>,
pub(crate) flags: Flags,
// This should probably not accept any expr to give better errors.
// Closures should be allowed though.
pub(crate) parser: Option<Expr>,
@@ -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");
}
}
+95
View File
@@ -0,0 +1,95 @@
use proc_macro2::TokenStream;
use quote::quote;
#[derive(Default)]
pub(crate) struct Flags {
pub short: Vec<char>,
pub long: Vec<String>,
}
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::<Vec<_>>()
.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
}
}
}
+22 -5
View File
@@ -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
}
}
);
+7
View File
@@ -48,6 +48,7 @@ impl From<lexopt::Error> for Error {
#[derive(Clone)]
pub enum Argument<T: Arguments> {
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<T: Arguments> {
@@ -96,6 +99,10 @@ impl<T: Arguments> ArgumentIter<T> {
pub fn help(&self) -> String {
T::help(self.parser.bin_name().unwrap())
}
pub fn version(&self) -> String {
T::version()
}
}
pub trait Options: Sized + Default {