diff --git a/derive/src/attributes.rs b/derive/src/attributes.rs index a4df626..3b5e453 100644 --- a/derive/src/attributes.rs +++ b/derive/src/attributes.rs @@ -30,7 +30,6 @@ enum AttributeArguments { Value(Expr), NumArgs(RangeInclusive), File(String), - Env(String), ExitCode(i32), Help(String), HelpFlags(Vec), @@ -123,28 +122,6 @@ impl OptionAttr { } } -#[derive(Default)] -pub(crate) struct FieldAttr { - pub(crate) default: Option, - pub(crate) env: Option, -} - -impl FieldAttr { - pub(crate) fn parse(attr: &Attribute) -> Self { - let mut field_attr = Self::default(); - - for arg in AttributeArguments::parse_all(attr) { - match arg { - AttributeArguments::Default(e) => field_attr.default = Some(e), - AttributeArguments::Env(e) => field_attr.env = Some(e), - _ => panic!("Invalid argument"), - }; - } - - field_attr - } -} - #[derive(Default)] pub(crate) struct ValueAttr { pub(crate) keys: Vec, @@ -265,7 +242,6 @@ impl Parse for AttributeArguments { "default" => return Ok(Self::Default(input.parse::()?)), "value" => return Ok(Self::Value(input.parse::()?)), "file" => return Ok(Self::File(input.parse::()?.value())), - "env" => return Ok(Self::Env(input.parse::()?.value())), "help" => return Ok(Self::Help(input.parse::()?.value())), "exit_code" => return Ok(Self::ExitCode(input.parse::()?.base10_parse()?)), "help_flags" => { @@ -313,6 +289,7 @@ impl Parse for AttributeArguments { _ => panic!("Unrecognized argument {name} for option attribute"), }; } + panic!("Arguments to option attribute must be string literals"); } } diff --git a/derive/src/field.rs b/derive/src/field.rs deleted file mode 100644 index ec3671f..0000000 --- a/derive/src/field.rs +++ /dev/null @@ -1,43 +0,0 @@ -use proc_macro2::TokenStream; -use quote::{quote, ToTokens}; -use syn::{Attribute, Field, Ident}; - -use crate::attributes::FieldAttr; - -pub(crate) struct FieldData { - pub(crate) ident: Ident, - pub(crate) default_value: TokenStream, -} - -pub(crate) fn parse_field(field: &Field) -> FieldData { - let field_ident = field.ident.as_ref().unwrap().clone(); - - let field_attr = parse_field_attr(&field.attrs); - - let mut default_value = match field_attr.default { - Some(val) => val.to_token_stream(), - None => quote!(::core::default::Default::default()), - }; - - if let Some(env_var) = field_attr.env { - default_value = quote!( - ::std::env::var_os(#env_var) - .and_then(|v| ::uutils_args::Value::from_value(&v).ok()) - .unwrap_or(#default_value) - ) - } - - FieldData { - ident: field_ident, - default_value, - } -} - -pub(crate) fn parse_field_attr(attrs: &[Attribute]) -> FieldAttr { - for attr in attrs { - if attr.path.is_ident("field") { - return FieldAttr::parse(attr); - } - } - FieldAttr::default() -} diff --git a/derive/src/initial.rs b/derive/src/initial.rs new file mode 100644 index 0000000..df8935b --- /dev/null +++ b/derive/src/initial.rs @@ -0,0 +1,127 @@ +use syn::{ + parse_macro_input, + Data::Struct, + DeriveInput, Fields, parse::{ParseStream, Parse}, Token, +}; + +use proc_macro::TokenStream; +use quote::quote; +use syn::{Attribute, Expr, LitStr, punctuated::Punctuated}; + +mod kw { + syn::custom_keyword!(env); +} + +enum InitialArg { + Expr(Expr), + Env(String), +} + +#[derive(Default)] +struct InitialField { + expr: Option, + env: Option, +} + +impl Parse for InitialArg { + fn parse(input: ParseStream) -> syn::Result { + if input.peek(kw::env) && input.peek2(Token![=]) { + input.parse::()?; + input.parse::()?; + Ok(InitialArg::Env(input.parse::()?.value())) + } else { + Ok(InitialArg::Expr(input.parse::()?)) + } + } +} + +impl InitialField { + fn from_attribute(attribute: &Attribute) -> syn::Result { + let mut _self = Self::default(); + + let args = attribute.parse_args_with(Punctuated::::parse_terminated)?; + + for arg in args { + match arg { + InitialArg::Expr(e) => { + if _self.expr.is_some() { + panic!("Can only specify one initial expression") + } + _self.expr = Some(e); + } + InitialArg::Env(s) => { + if _self.expr.is_some() { + panic!("Can only specify one env variable") + } + _self.env = Some(s); + } + } + } + + Ok(_self) + } + + fn to_expr(self) -> proc_macro2::TokenStream { + let mut default_value = match self.expr { + Some(val) => quote!(#val.into()), + None => quote!(::core::default::Default::default()), + }; + + if let Some(env_var) = self.env { + default_value = quote!( + ::std::env::var_os(#env_var) + .and_then(|v| ::uutils_args::Value::from_value(&v).ok()) + .unwrap_or(#default_value) + ); + } + default_value.into() + } +} + +pub fn initial(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + let name = input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + + let Struct(data) = input.data else { + panic!("Input should be a struct!"); + }; + + let Fields::Named(fields) = data.fields else { + panic!("Fields must be named"); + }; + + // The key of this map is a literal pattern and the value + // is whatever code needs to be run when that pattern is encountered. + let mut defaults = Vec::new(); + for field in fields.named { + let ident = field.ident; + let field = parse_field_attr(&field.attrs); + let default_value = field.to_expr(); + + defaults.push(quote!(#ident: #default_value)); + } + + let expanded = quote!( + impl #impl_generics Initial for #name #ty_generics #where_clause { + fn initial() -> Self { + Self { + #(#defaults),* + } + } + } + ); + + TokenStream::from(expanded) +} + +fn parse_field_attr(attrs: &[Attribute]) -> InitialField { + for attr in attrs { + if attr.path.is_ident("initial") { + return InitialField::from_attribute(attr).expect("Failed to parse initial attribute"); + } + } + InitialField::default() +} + diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 448df00..69264e3 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -1,64 +1,28 @@ mod argument; mod attributes; -mod field; mod flags; mod help; mod markdown; +mod initial; use argument::{ long_handling, number_handling, parse_argument, parse_arguments_attr, positional_handling, short_handling, }; use attributes::ValueAttr; -use field::{parse_field, FieldData}; use help::{help_handling, help_string, version_handling}; use proc_macro::TokenStream; use quote::quote; use syn::{ parse_macro_input, - Data::{Enum, Struct}, - DeriveInput, Fields, + Data::Enum, + DeriveInput, }; -#[proc_macro_derive(Initial, attributes(field))] +#[proc_macro_derive(Initial, attributes(initial))] pub fn initial(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - - let name = input.ident; - let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); - - let Struct(data) = input.data else { - panic!("Input should be a struct!"); - }; - - let Fields::Named(fields) = data.fields else { - panic!("Fields must be named"); - }; - - // The key of this map is a literal pattern and the value - // is whatever code needs to be run when that pattern is encountered. - let mut defaults = Vec::new(); - for field in fields.named { - let FieldData { - ident, - default_value, - } = parse_field(&field); - - defaults.push(quote!(#ident: #default_value)); - } - - let expanded = quote!( - impl #impl_generics Initial for #name #ty_generics #where_clause { - fn initial() -> Self { - Self { - #(#defaults),* - } - } - } - ); - - TokenStream::from(expanded) + initial::initial(input) } #[proc_macro_derive(Arguments, attributes(flag, option, positional, arguments))] diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 539ff4b..a225951 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -23,7 +23,7 @@ enum Arg { #[derive(Initial)] struct Settings { name: String, - #[field(default = 1)] + #[initial(1)] count: u8, } diff --git a/tests/coreutils.rs b/tests/coreutils.rs index f70261b..98634d1 100644 --- a/tests/coreutils.rs +++ b/tests/coreutils.rs @@ -27,3 +27,6 @@ mod ls; #[path = "coreutils/tail.rs"] mod tail; + +#[path = "coreutils/uniq.rs"] +mod uniq; diff --git a/tests/coreutils/base32.rs b/tests/coreutils/base32.rs index 03120b5..240d9d0 100644 --- a/tests/coreutils/base32.rs +++ b/tests/coreutils/base32.rs @@ -21,7 +21,7 @@ enum Arg { struct Settings { decode: bool, ignore_garbage: bool, - #[field(default = Some(76))] + #[initial(Some(76))] wrap: Option, file: Option, } diff --git a/tests/coreutils/ls.rs b/tests/coreutils/ls.rs index 46273bd..4a71000 100644 --- a/tests/coreutils/ls.rs +++ b/tests/coreutils/ls.rs @@ -322,14 +322,14 @@ struct Settings { long_numeric_uid_gid: bool, // alloc_size: bool, // block_size: Option, - #[field(default = default_terminal_size())] + #[initial(default_terminal_size())] width: u16, quoting_style: QuotingStyle, indicator_style: IndicatorStyle, // time_style: TimeStyle, context: bool, group_directories_first: bool, - #[field(default = '\n')] + #[initial('\n')] eol: char, which_files: Files, ignore_backups: bool, diff --git a/tests/defaults.rs b/tests/defaults.rs index 44a31ef..1ef9a9d 100644 --- a/tests/defaults.rs +++ b/tests/defaults.rs @@ -10,7 +10,7 @@ fn true_default() { #[derive(Initial)] struct Settings { - #[field(default = true)] + #[initial(true)] foo: bool, } @@ -34,7 +34,7 @@ fn env_var_string() { #[derive(Initial)] struct Settings { - #[field(env = "FOO")] + #[initial("FOO")] foo: String, } diff --git a/tests/options.rs b/tests/options.rs index 03c1133..6b094b7 100644 --- a/tests/options.rs +++ b/tests/options.rs @@ -170,7 +170,7 @@ fn color() { #[derive(Initial)] struct Settings { - #[field(default = Color::Auto)] + #[initial(Color::Auto)] color: Color, }