diff --git a/onlyargs_derive/compile_tests/compiler.rs b/onlyargs_derive/compile_tests/compiler.rs index 740489c..106dd5b 100644 --- a/onlyargs_derive/compile_tests/compiler.rs +++ b/onlyargs_derive/compile_tests/compiler.rs @@ -32,6 +32,18 @@ fn compile_tests() { t.pass("compile_tests/positional_usize.rs"); t.compile_fail("compile_tests/conflicting_positional.rs"); + t.pass("compile_tests/multivalue_f32.rs"); + t.pass("compile_tests/multivalue_f64.rs"); + t.pass("compile_tests/multivalue_i8.rs"); + t.pass("compile_tests/multivalue_i128.rs"); + t.pass("compile_tests/multivalue_isize.rs"); + t.pass("compile_tests/multivalue_u8.rs"); + t.pass("compile_tests/multivalue_u128.rs"); + t.pass("compile_tests/multivalue_usize.rs"); + t.pass("compile_tests/multivalue_osstring.rs"); + t.pass("compile_tests/multivalue_pathbuf.rs"); + t.pass("compile_tests/multivalue_string.rs"); + t.pass("compile_tests/empty.rs"); t.pass("compile_tests/optional.rs"); t.pass("compile_tests/struct_doc_comment.rs"); @@ -40,4 +52,15 @@ fn compile_tests() { t.compile_fail("compile_tests/conflicting_short_name.rs"); t.pass("compile_tests/manual_short_name.rs"); t.pass("compile_tests/ignore_short_name.rs"); + + // Various expected errors. + t.compile_fail("compile_tests/required_bool.rs"); + t.compile_fail("compile_tests/required_option.rs"); + t.compile_fail("compile_tests/required_string.rs"); + t.compile_fail("compile_tests/default_multivalue.rs"); + t.compile_fail("compile_tests/default_option.rs"); + t.compile_fail("compile_tests/default_positional.rs"); + t.compile_fail("compile_tests/positional_option.rs"); + t.compile_fail("compile_tests/positional_single_bool.rs"); + t.compile_fail("compile_tests/positional_single_string.rs"); } diff --git a/onlyargs_derive/compile_tests/conflicting_positional.rs b/onlyargs_derive/compile_tests/conflicting_positional.rs index 3b9f2b2..20eb5dc 100644 --- a/onlyargs_derive/compile_tests/conflicting_positional.rs +++ b/onlyargs_derive/compile_tests/conflicting_positional.rs @@ -1,6 +1,8 @@ #[derive(Debug, onlyargs_derive::OnlyArgs)] struct Args { + #[positional] rest: Vec, + #[positional] more: Vec, } diff --git a/onlyargs_derive/compile_tests/conflicting_positional.stderr b/onlyargs_derive/compile_tests/conflicting_positional.stderr index f385a2c..43888c2 100644 --- a/onlyargs_derive/compile_tests/conflicting_positional.stderr +++ b/onlyargs_derive/compile_tests/conflicting_positional.stderr @@ -1,5 +1,5 @@ error: Positional arguments can only be specified once. - --> compile_tests/conflicting_positional.rs:4:5 + --> compile_tests/conflicting_positional.rs:6:5 | -4 | more: Vec, +6 | more: Vec, | ^^^^ diff --git a/onlyargs_derive/compile_tests/default_multivalue.rs b/onlyargs_derive/compile_tests/default_multivalue.rs new file mode 100644 index 0000000..cc43406 --- /dev/null +++ b/onlyargs_derive/compile_tests/default_multivalue.rs @@ -0,0 +1,7 @@ +#[derive(Debug, onlyargs_derive::OnlyArgs)] +struct Args { + #[default(123)] + nums: Vec, +} + +fn main() {} diff --git a/onlyargs_derive/compile_tests/default_multivalue.stderr b/onlyargs_derive/compile_tests/default_multivalue.stderr new file mode 100644 index 0000000..8db121a --- /dev/null +++ b/onlyargs_derive/compile_tests/default_multivalue.stderr @@ -0,0 +1,5 @@ +error: #[default(...)] can only be used on primitive types + --> compile_tests/default_multivalue.rs:4:11 + | +4 | nums: Vec, + | ^^^ diff --git a/onlyargs_derive/compile_tests/default_option.rs b/onlyargs_derive/compile_tests/default_option.rs new file mode 100644 index 0000000..718eb15 --- /dev/null +++ b/onlyargs_derive/compile_tests/default_option.rs @@ -0,0 +1,7 @@ +#[derive(Debug, onlyargs_derive::OnlyArgs)] +struct Args { + #[default(123)] + opt_num: Option, +} + +fn main() {} diff --git a/onlyargs_derive/compile_tests/default_option.stderr b/onlyargs_derive/compile_tests/default_option.stderr new file mode 100644 index 0000000..a3ee2a9 --- /dev/null +++ b/onlyargs_derive/compile_tests/default_option.stderr @@ -0,0 +1,5 @@ +error: #[default(...)] can only be used on primitive types + --> compile_tests/default_option.rs:4:14 + | +4 | opt_num: Option, + | ^^^^^^ diff --git a/onlyargs_derive/compile_tests/default_positional.rs b/onlyargs_derive/compile_tests/default_positional.rs new file mode 100644 index 0000000..d22710d --- /dev/null +++ b/onlyargs_derive/compile_tests/default_positional.rs @@ -0,0 +1,8 @@ +#[derive(Debug, onlyargs_derive::OnlyArgs)] +struct Args { + #[positional] + #[default(123)] + nums: Vec, +} + +fn main() {} diff --git a/onlyargs_derive/compile_tests/default_positional.stderr b/onlyargs_derive/compile_tests/default_positional.stderr new file mode 100644 index 0000000..7797d7e --- /dev/null +++ b/onlyargs_derive/compile_tests/default_positional.stderr @@ -0,0 +1,5 @@ +error: #[default(...)] can only be used on primitive types + --> compile_tests/default_positional.rs:5:11 + | +5 | nums: Vec, + | ^^^ diff --git a/onlyargs_derive/compile_tests/multivalue_f32.rs b/onlyargs_derive/compile_tests/multivalue_f32.rs new file mode 100644 index 0000000..558bf2f --- /dev/null +++ b/onlyargs_derive/compile_tests/multivalue_f32.rs @@ -0,0 +1,6 @@ +#[derive(Debug, onlyargs_derive::OnlyArgs)] +struct Args { + vertices: Vec, +} + +fn main() {} diff --git a/onlyargs_derive/compile_tests/multivalue_f64.rs b/onlyargs_derive/compile_tests/multivalue_f64.rs new file mode 100644 index 0000000..9c00c9a --- /dev/null +++ b/onlyargs_derive/compile_tests/multivalue_f64.rs @@ -0,0 +1,6 @@ +#[derive(Debug, onlyargs_derive::OnlyArgs)] +struct Args { + vertices: Vec, +} + +fn main() {} diff --git a/onlyargs_derive/compile_tests/multivalue_i128.rs b/onlyargs_derive/compile_tests/multivalue_i128.rs new file mode 100644 index 0000000..e9a9696 --- /dev/null +++ b/onlyargs_derive/compile_tests/multivalue_i128.rs @@ -0,0 +1,6 @@ +#[derive(Debug, onlyargs_derive::OnlyArgs)] +struct Args { + vertices: Vec, +} + +fn main() {} diff --git a/onlyargs_derive/compile_tests/multivalue_i8.rs b/onlyargs_derive/compile_tests/multivalue_i8.rs new file mode 100644 index 0000000..e32ad5d --- /dev/null +++ b/onlyargs_derive/compile_tests/multivalue_i8.rs @@ -0,0 +1,6 @@ +#[derive(Debug, onlyargs_derive::OnlyArgs)] +struct Args { + vertices: Vec, +} + +fn main() {} diff --git a/onlyargs_derive/compile_tests/multivalue_isize.rs b/onlyargs_derive/compile_tests/multivalue_isize.rs new file mode 100644 index 0000000..b29dfef --- /dev/null +++ b/onlyargs_derive/compile_tests/multivalue_isize.rs @@ -0,0 +1,6 @@ +#[derive(Debug, onlyargs_derive::OnlyArgs)] +struct Args { + vertices: Vec, +} + +fn main() {} diff --git a/onlyargs_derive/compile_tests/multivalue_osstring.rs b/onlyargs_derive/compile_tests/multivalue_osstring.rs new file mode 100644 index 0000000..1aa8a07 --- /dev/null +++ b/onlyargs_derive/compile_tests/multivalue_osstring.rs @@ -0,0 +1,6 @@ +#[derive(Debug, onlyargs_derive::OnlyArgs)] +struct Args { + strings: Vec, +} + +fn main() {} diff --git a/onlyargs_derive/compile_tests/multivalue_pathbuf.rs b/onlyargs_derive/compile_tests/multivalue_pathbuf.rs new file mode 100644 index 0000000..ca0c640 --- /dev/null +++ b/onlyargs_derive/compile_tests/multivalue_pathbuf.rs @@ -0,0 +1,6 @@ +#[derive(Debug, onlyargs_derive::OnlyArgs)] +struct Args { + paths: Vec, +} + +fn main() {} diff --git a/onlyargs_derive/compile_tests/multivalue_string.rs b/onlyargs_derive/compile_tests/multivalue_string.rs new file mode 100644 index 0000000..284cb6b --- /dev/null +++ b/onlyargs_derive/compile_tests/multivalue_string.rs @@ -0,0 +1,6 @@ +#[derive(Debug, onlyargs_derive::OnlyArgs)] +struct Args { + strings: Vec, +} + +fn main() {} diff --git a/onlyargs_derive/compile_tests/multivalue_u128.rs b/onlyargs_derive/compile_tests/multivalue_u128.rs new file mode 100644 index 0000000..5bc41ce --- /dev/null +++ b/onlyargs_derive/compile_tests/multivalue_u128.rs @@ -0,0 +1,6 @@ +#[derive(Debug, onlyargs_derive::OnlyArgs)] +struct Args { + vertices: Vec, +} + +fn main() {} diff --git a/onlyargs_derive/compile_tests/multivalue_u8.rs b/onlyargs_derive/compile_tests/multivalue_u8.rs new file mode 100644 index 0000000..e32ad5d --- /dev/null +++ b/onlyargs_derive/compile_tests/multivalue_u8.rs @@ -0,0 +1,6 @@ +#[derive(Debug, onlyargs_derive::OnlyArgs)] +struct Args { + vertices: Vec, +} + +fn main() {} diff --git a/onlyargs_derive/compile_tests/multivalue_usize.rs b/onlyargs_derive/compile_tests/multivalue_usize.rs new file mode 100644 index 0000000..079bc3c --- /dev/null +++ b/onlyargs_derive/compile_tests/multivalue_usize.rs @@ -0,0 +1,6 @@ +#[derive(Debug, onlyargs_derive::OnlyArgs)] +struct Args { + vertices: Vec, +} + +fn main() {} diff --git a/onlyargs_derive/compile_tests/positional_f32.rs b/onlyargs_derive/compile_tests/positional_f32.rs index 126c15d..0dfdac1 100644 --- a/onlyargs_derive/compile_tests/positional_f32.rs +++ b/onlyargs_derive/compile_tests/positional_f32.rs @@ -1,5 +1,6 @@ #[derive(Debug, onlyargs_derive::OnlyArgs)] struct Args { + #[positional] rest: Vec, } diff --git a/onlyargs_derive/compile_tests/positional_f64.rs b/onlyargs_derive/compile_tests/positional_f64.rs index 63dace4..b7fbb9c 100644 --- a/onlyargs_derive/compile_tests/positional_f64.rs +++ b/onlyargs_derive/compile_tests/positional_f64.rs @@ -1,5 +1,6 @@ #[derive(Debug, onlyargs_derive::OnlyArgs)] struct Args { + #[positional] rest: Vec, } diff --git a/onlyargs_derive/compile_tests/positional_i128.rs b/onlyargs_derive/compile_tests/positional_i128.rs index e65bfae..078ff03 100644 --- a/onlyargs_derive/compile_tests/positional_i128.rs +++ b/onlyargs_derive/compile_tests/positional_i128.rs @@ -1,5 +1,6 @@ #[derive(Debug, onlyargs_derive::OnlyArgs)] struct Args { + #[positional] rest: Vec, } diff --git a/onlyargs_derive/compile_tests/positional_i8.rs b/onlyargs_derive/compile_tests/positional_i8.rs index d584cd8..20ceccd 100644 --- a/onlyargs_derive/compile_tests/positional_i8.rs +++ b/onlyargs_derive/compile_tests/positional_i8.rs @@ -1,5 +1,6 @@ #[derive(Debug, onlyargs_derive::OnlyArgs)] struct Args { + #[positional] rest: Vec, } diff --git a/onlyargs_derive/compile_tests/positional_isize.rs b/onlyargs_derive/compile_tests/positional_isize.rs index b616ed6..6de9001 100644 --- a/onlyargs_derive/compile_tests/positional_isize.rs +++ b/onlyargs_derive/compile_tests/positional_isize.rs @@ -1,5 +1,6 @@ #[derive(Debug, onlyargs_derive::OnlyArgs)] struct Args { + #[positional] rest: Vec, } diff --git a/onlyargs_derive/compile_tests/positional_option.rs b/onlyargs_derive/compile_tests/positional_option.rs new file mode 100644 index 0000000..a8e1aae --- /dev/null +++ b/onlyargs_derive/compile_tests/positional_option.rs @@ -0,0 +1,7 @@ +#[derive(Debug, onlyargs_derive::OnlyArgs)] +struct Args { + #[positional] + rest: Option, +} + +fn main() {} diff --git a/onlyargs_derive/compile_tests/positional_option.stderr b/onlyargs_derive/compile_tests/positional_option.stderr new file mode 100644 index 0000000..57a3776 --- /dev/null +++ b/onlyargs_derive/compile_tests/positional_option.stderr @@ -0,0 +1,5 @@ +error: #[positional] can only be used on `Vec` + --> compile_tests/positional_option.rs:4:11 + | +4 | rest: Option, + | ^^^^^^ diff --git a/onlyargs_derive/compile_tests/positional_osstring.rs b/onlyargs_derive/compile_tests/positional_osstring.rs index 5825add..d891e8d 100644 --- a/onlyargs_derive/compile_tests/positional_osstring.rs +++ b/onlyargs_derive/compile_tests/positional_osstring.rs @@ -1,5 +1,6 @@ #[derive(Debug, onlyargs_derive::OnlyArgs)] struct Args { + #[positional] rest: Vec, } diff --git a/onlyargs_derive/compile_tests/positional_pathbuf.rs b/onlyargs_derive/compile_tests/positional_pathbuf.rs index 4fe1d1b..0fcf267 100644 --- a/onlyargs_derive/compile_tests/positional_pathbuf.rs +++ b/onlyargs_derive/compile_tests/positional_pathbuf.rs @@ -1,5 +1,6 @@ #[derive(Debug, onlyargs_derive::OnlyArgs)] struct Args { + #[positional] rest: Vec, } diff --git a/onlyargs_derive/compile_tests/positional_single_bool.rs b/onlyargs_derive/compile_tests/positional_single_bool.rs new file mode 100644 index 0000000..b671b64 --- /dev/null +++ b/onlyargs_derive/compile_tests/positional_single_bool.rs @@ -0,0 +1,7 @@ +#[derive(Debug, onlyargs_derive::OnlyArgs)] +struct Args { + #[positional] + rest: bool, +} + +fn main() {} diff --git a/onlyargs_derive/compile_tests/positional_single_bool.stderr b/onlyargs_derive/compile_tests/positional_single_bool.stderr new file mode 100644 index 0000000..644d61a --- /dev/null +++ b/onlyargs_derive/compile_tests/positional_single_bool.stderr @@ -0,0 +1,5 @@ +error: #[positional] can only be used on `Vec` + --> compile_tests/positional_single_bool.rs:4:11 + | +4 | rest: bool, + | ^^^^ diff --git a/onlyargs_derive/compile_tests/positional_single_string.rs b/onlyargs_derive/compile_tests/positional_single_string.rs new file mode 100644 index 0000000..bf828d3 --- /dev/null +++ b/onlyargs_derive/compile_tests/positional_single_string.rs @@ -0,0 +1,7 @@ +#[derive(Debug, onlyargs_derive::OnlyArgs)] +struct Args { + #[positional] + rest: String, +} + +fn main() {} diff --git a/onlyargs_derive/compile_tests/positional_single_string.stderr b/onlyargs_derive/compile_tests/positional_single_string.stderr new file mode 100644 index 0000000..47a8f8d --- /dev/null +++ b/onlyargs_derive/compile_tests/positional_single_string.stderr @@ -0,0 +1,5 @@ +error: #[positional] can only be used on `Vec` + --> compile_tests/positional_single_string.rs:4:11 + | +4 | rest: String, + | ^^^^^^ diff --git a/onlyargs_derive/compile_tests/positional_string.rs b/onlyargs_derive/compile_tests/positional_string.rs index a7f8c50..3253263 100644 --- a/onlyargs_derive/compile_tests/positional_string.rs +++ b/onlyargs_derive/compile_tests/positional_string.rs @@ -1,5 +1,6 @@ #[derive(Debug, onlyargs_derive::OnlyArgs)] struct Args { + #[positional] rest: Vec, } diff --git a/onlyargs_derive/compile_tests/positional_u128.rs b/onlyargs_derive/compile_tests/positional_u128.rs index fe600e8..080c8df 100644 --- a/onlyargs_derive/compile_tests/positional_u128.rs +++ b/onlyargs_derive/compile_tests/positional_u128.rs @@ -1,5 +1,6 @@ #[derive(Debug, onlyargs_derive::OnlyArgs)] struct Args { + #[positional] rest: Vec, } diff --git a/onlyargs_derive/compile_tests/positional_u8.rs b/onlyargs_derive/compile_tests/positional_u8.rs index 3dc0aef..dff0eff 100644 --- a/onlyargs_derive/compile_tests/positional_u8.rs +++ b/onlyargs_derive/compile_tests/positional_u8.rs @@ -1,5 +1,6 @@ #[derive(Debug, onlyargs_derive::OnlyArgs)] struct Args { + #[positional] rest: Vec, } diff --git a/onlyargs_derive/compile_tests/positional_usize.rs b/onlyargs_derive/compile_tests/positional_usize.rs index 2dd6735..e798304 100644 --- a/onlyargs_derive/compile_tests/positional_usize.rs +++ b/onlyargs_derive/compile_tests/positional_usize.rs @@ -1,5 +1,6 @@ #[derive(Debug, onlyargs_derive::OnlyArgs)] struct Args { + #[positional] rest: Vec, } diff --git a/onlyargs_derive/compile_tests/required_bool.rs b/onlyargs_derive/compile_tests/required_bool.rs new file mode 100644 index 0000000..ec5a895 --- /dev/null +++ b/onlyargs_derive/compile_tests/required_bool.rs @@ -0,0 +1,7 @@ +#[derive(Debug, onlyargs_derive::OnlyArgs)] +struct Args { + #[required] + required_bool: bool, +} + +fn main() {} diff --git a/onlyargs_derive/compile_tests/required_bool.stderr b/onlyargs_derive/compile_tests/required_bool.stderr new file mode 100644 index 0000000..baf7238 --- /dev/null +++ b/onlyargs_derive/compile_tests/required_bool.stderr @@ -0,0 +1,5 @@ +error: #[required] can only be used on `Vec` + --> compile_tests/required_bool.rs:4:20 + | +4 | required_bool: bool, + | ^^^^ diff --git a/onlyargs_derive/compile_tests/required_option.rs b/onlyargs_derive/compile_tests/required_option.rs new file mode 100644 index 0000000..531cd44 --- /dev/null +++ b/onlyargs_derive/compile_tests/required_option.rs @@ -0,0 +1,7 @@ +#[derive(Debug, onlyargs_derive::OnlyArgs)] +struct Args { + #[required] + required_option: Option, +} + +fn main() {} diff --git a/onlyargs_derive/compile_tests/required_option.stderr b/onlyargs_derive/compile_tests/required_option.stderr new file mode 100644 index 0000000..a0e0428 --- /dev/null +++ b/onlyargs_derive/compile_tests/required_option.stderr @@ -0,0 +1,5 @@ +error: #[required] can only be used on `Vec` + --> compile_tests/required_option.rs:4:22 + | +4 | required_option: Option, + | ^^^^^^ diff --git a/onlyargs_derive/compile_tests/required_string.rs b/onlyargs_derive/compile_tests/required_string.rs new file mode 100644 index 0000000..c115377 --- /dev/null +++ b/onlyargs_derive/compile_tests/required_string.rs @@ -0,0 +1,7 @@ +#[derive(Debug, onlyargs_derive::OnlyArgs)] +struct Args { + #[required] + required_string: String, +} + +fn main() {} diff --git a/onlyargs_derive/compile_tests/required_string.stderr b/onlyargs_derive/compile_tests/required_string.stderr new file mode 100644 index 0000000..558ae14 --- /dev/null +++ b/onlyargs_derive/compile_tests/required_string.stderr @@ -0,0 +1,5 @@ +error: #[required] can only be used on `Vec` + --> compile_tests/required_string.rs:4:22 + | +4 | required_string: String, + | ^^^^^^ diff --git a/onlyargs_derive/src/lib.rs b/onlyargs_derive/src/lib.rs index e9c2218..3568b90 100644 --- a/onlyargs_derive/src/lib.rs +++ b/onlyargs_derive/src/lib.rs @@ -51,6 +51,11 @@ //! - This behavior can be suppressed with the `#[long]` attribute (see below). //! - Alternatively, the `#[short('…')]` attribute can be used to set a specific short name. //! +//! # Footer +//! +//! The `#[footer = "..."]` attribute on the argument struct will add lines to the bottom of the +//! help message. It can be used multiple times. +//! //! # Provided arguments //! //! `--help|-h` and `--version|-V` arguments are automatically generated. When the parser encounters @@ -69,6 +74,9 @@ //! - Accepts string literals for `PathBuf`. //! - Accepts numeric literals for numeric types. //! - Accepts `true` and `false` idents and `"true"` and `"false"` string literals for `boolean`. +//! - `#[required]`: Can be used on `Vec` to require at least one value. This ensures the vector +//! is never empty. +//! - `#[positional]`: Makes a `Vec` the dumping ground for positional arguments. //! //! # Supported types //! @@ -91,10 +99,10 @@ //! Additionally, some wrapper and composite types are also available, where the type `T` must be //! one of the primitive types listed above (except `bool`). //! -//! | Type | Description | -//! |-------------|-----------------------------------| -//! | `Option` | An optional argument. | -//! | `Vec` | Positional arguments (see below). | +//! | Type | Description | +//! |-------------|------------------------------------------------------------| +//! | `Option` | An optional argument. | +//! | `Vec` | Multivalue and positional arguments (see `#[positional]`). | //! //! In argument parsing parlance, "flags" are simple boolean values; the argument does not require //! a value. For example, the argument `--help`. @@ -102,18 +110,15 @@ //! "Options" carry a value and the argument parser requires the value to directly follow the //! argument name. Arguments can be made optional with `Option`. //! -//! ## Positional arguments -//! -//! If the struct contains a field with a vector type, it _must_ be the only vector field. This -//! becomes the "dumping ground" for all positional arguments, which are any args that do not match -//! an existing field, or any arguments following the `--` "stop parsing" sentinel. +//! Multivalue arguments can be passed on the command line by using the same argument multiple +//! times. #![forbid(unsafe_code)] #![deny(clippy::all)] #![deny(clippy::pedantic)] #![allow(clippy::let_underscore_untyped)] -use crate::parser::{ArgFlag, ArgOption, ArgType, ArgView, ArgumentStruct}; +use crate::parser::{ArgFlag, ArgOption, ArgProperty, ArgType, ArgView, ArgumentStruct}; use myn::utils::spanned_error; use proc_macro::{Ident, Span, TokenStream}; use std::{collections::HashMap, fmt::Write as _, str::FromStr as _}; @@ -122,7 +127,10 @@ mod parser; /// See the [root module documentation](crate) for the DSL specification. #[allow(clippy::too_many_lines)] -#[proc_macro_derive(OnlyArgs, attributes(footer, default, long, short))] +#[proc_macro_derive( + OnlyArgs, + attributes(footer, default, long, positional, required, short) +)] pub fn derive_parser(input: TokenStream) -> TokenStream { let ast = match ArgumentStruct::parse(input) { Ok(ast) => ast, @@ -204,7 +212,15 @@ pub fn derive_parser(input: TokenStream) -> TokenStream { if let Some(default) = opt.default.as_ref() { format!("let mut {name} = {default}{};", opt.ty_help.converter()) } else { - format!("let mut {name} = None;") + match opt.property { + ArgProperty::Optional | ArgProperty::Required => { + format!("let mut {name} = None;") + } + ArgProperty::MultiValue { .. } => { + format!("let mut {name} = vec![];") + } + ArgProperty::Positional { .. } => unreachable!(), + } } }) .collect::(); @@ -243,27 +259,41 @@ pub fn derive_parser(input: TokenStream) -> TokenStream { .short .map(|ch| format!(r#"| Some(arg_name_ @ "-{ch}")"#)) .unwrap_or_default(); - let value = if opt.default.is_some() { + let assignment = if opt.default.is_some() { match opt.ty_help { - ArgType::Float => "args.next().parse_float(arg_name_)?", - ArgType::Integer => "args.next().parse_int(arg_name_)?", - ArgType::OsString => "args.next().parse_osstr(arg_name_)?", - ArgType::Path => "args.next().parse_path(arg_name_)?", - ArgType::String => "args.next().parse_str(arg_name_)?", + ArgType::Float => format!("{name} = args.next().parse_float(arg_name_)?"), + ArgType::Integer => format!("{name} = args.next().parse_int(arg_name_)?"), + ArgType::OsString => format!("{name} = args.next().parse_osstr(arg_name_)?"), + ArgType::Path => format!("{name} = args.next().parse_path(arg_name_)?"), + ArgType::String => format!("{name} = args.next().parse_str(arg_name_)?"), } } else { - match opt.ty_help { - ArgType::Float => "Some(args.next().parse_float(arg_name_)?)", - ArgType::Integer => "Some(args.next().parse_int(arg_name_)?)", - ArgType::OsString => "Some(args.next().parse_osstr(arg_name_)?)", - ArgType::Path => "Some(args.next().parse_path(arg_name_)?)", - ArgType::String => "Some(args.next().parse_str(arg_name_)?)", + match opt.property { + ArgProperty::Optional | ArgProperty::Required => match opt.ty_help { + ArgType::Float => format!("{name} = Some(args.next().parse_float(arg_name_)?)"), + ArgType::Integer => format!("{name} = Some(args.next().parse_int(arg_name_)?)"), + ArgType::OsString => { + format!("{name} = Some(args.next().parse_osstr(arg_name_)?)") + } + ArgType::Path => format!("{name} = Some(args.next().parse_path(arg_name_)?)"), + ArgType::String => format!("{name} = Some(args.next().parse_str(arg_name_)?)"), + }, + ArgProperty::MultiValue { .. } => match opt.ty_help { + ArgType::Float => format!("{name}.push(args.next().parse_float(arg_name_)?)"), + ArgType::Integer => format!("{name}.push(args.next().parse_int(arg_name_)?)"), + ArgType::OsString => { + format!("{name}.push(args.next().parse_osstr(arg_name_)?)") + } + ArgType::Path => format!("{name}.push(args.next().parse_path(arg_name_)?)"), + ArgType::String => format!("{name}.push(args.next().parse_str(arg_name_)?)"), + }, + ArgProperty::Positional { .. } => unreachable!(), } }; write!( matchers, - r#"Some(arg_name_ @ "--{arg}") {short} => {name} = {value},"#, + r#"Some(arg_name_ @ "--{arg}") {short} => {assignment},"#, arg = to_arg_name(name) ) .unwrap(); @@ -309,7 +339,13 @@ pub fn derive_parser(input: TokenStream) -> TokenStream { .iter() .map(|opt| { let name = &opt.name; - if opt.default.is_some() || opt.optional { + let optional = matches!( + opt.property, + ArgProperty::Optional + | ArgProperty::Positional { required: false } + | ArgProperty::MultiValue { required: false } + ); + if opt.default.is_some() || optional { format!("{name},") } else { format!( diff --git a/onlyargs_derive/src/parser.rs b/onlyargs_derive/src/parser.rs index 59d823b..317d299 100644 --- a/onlyargs_derive/src/parser.rs +++ b/onlyargs_derive/src/parser.rs @@ -1,5 +1,5 @@ use myn::prelude::*; -use proc_macro::{Delimiter, Ident, Literal, TokenStream}; +use proc_macro::{Delimiter, Ident, Literal, Span, TokenStream}; #[derive(Debug)] pub(crate) struct ArgumentStruct { @@ -33,8 +33,7 @@ pub(crate) struct ArgOption { pub(crate) ty_help: ArgType, pub(crate) doc: Vec, pub(crate) default: Option, - pub(crate) optional: bool, - pub(crate) positional: bool, + pub(crate) property: ArgProperty, } #[derive(Copy, Clone, Debug)] @@ -54,6 +53,14 @@ pub(crate) enum ArgType { String, } +#[derive(Copy, Clone, Debug)] +pub(crate) enum ArgProperty { + Required, + Optional, + MultiValue { required: bool }, + Positional { required: bool }, +} + impl ArgumentStruct { pub(crate) fn parse(input: TokenStream) -> Result { let mut input = input.into_token_iter(); @@ -72,9 +79,9 @@ impl ArgumentStruct { for field in fields { match field { Argument::Flag(flag) => flags.push(flag), - Argument::Option(opt) => match (opt.positional, &positional) { - (true, None) => positional = Some(opt), - (true, Some(_)) => { + Argument::Option(opt) => match (opt.property, &positional) { + (ArgProperty::Positional { .. }, None) => positional = Some(opt), + (ArgProperty::Positional { .. }, Some(_)) => { return Err(spanned_error( "Positional arguments can only be specified once.", opt.name.span(), @@ -124,6 +131,8 @@ impl Argument { let mut default = None; let mut long = false; let mut short = None; + let mut required = false; + let mut positional = false; for mut attr in attrs { let name = attr.name.to_string(); @@ -141,6 +150,8 @@ impl Argument { })?); } "long" => long = true, + "positional" => positional = true, + "required" => required = true, "short" => { let mut stream = attr.tree.expect_group(Delimiter::Parenthesis)?; let lit = stream.try_lit()?; @@ -167,6 +178,19 @@ impl Argument { }; if path == "bool" { + if required { + return Err(spanned_error( + "#[required] can only be used on `Vec`", + span, + )); + } + if positional { + return Err(spanned_error( + "#[positional] can only be used on `Vec`", + span, + )); + } + let mut flag = ArgFlag::new(name, short, doc); match default { Some(lit) if lit.to_string() == r#""true""# => flag.default = true, @@ -174,23 +198,25 @@ impl Argument { } args.push(Self::Flag(flag)); } else { - let mut opt = ArgOption::new(name, short, doc, &path).map_err(|()| { - spanned_error( - "Expected bool, PathBuf, String, OsString, integer, or float", - span, - ) - })?; + let mut opt = ArgOption::new(span, name, short, doc, &path)?; + + apply_default(span, &mut opt, default)?; + apply_required(span, &mut opt, required)?; + apply_positional(span, &mut opt, positional)?; - opt.default = default; if let Some(default) = opt.default.as_ref() { - opt.optional = false; let default = default.to_string(); if let Some(line) = opt.doc.last_mut() { line.push_str(&format!(" [default: {default}]")); } else { opt.doc.push(format!("[default: {default}]")); } - } else if !opt.optional { + } else if matches!( + opt.property, + ArgProperty::Required + | ArgProperty::Positional { required: true } + | ArgProperty::MultiValue { required: true } + ) { if let Some(line) = opt.doc.last_mut() { line.push_str(" [required]"); } else { @@ -206,6 +232,59 @@ impl Argument { } } +fn apply_default( + span: Span, + opt: &mut ArgOption, + default: Option, +) -> Result<(), TokenStream> { + match (default.is_some(), &opt.property) { + (true, ArgProperty::Required) => opt.default = default, + (true, _) => { + return Err(spanned_error( + "#[default(...)] can only be used on primitive types", + span, + )); + } + (false, _) => (), + } + + Ok(()) +} + +fn apply_required(span: Span, opt: &mut ArgOption, required: bool) -> Result<(), TokenStream> { + match (required, &mut opt.property) { + (false, _) => (), + (true, ArgProperty::MultiValue { required }) => *required = true, + _ => { + return Err(spanned_error( + "#[required] can only be used on `Vec`", + span, + )); + } + } + + Ok(()) +} + +fn apply_positional(span: Span, opt: &mut ArgOption, positional: bool) -> Result<(), TokenStream> { + match (positional, &opt.property) { + (true, ArgProperty::MultiValue { required }) => { + opt.property = ArgProperty::Positional { + required: *required, + } + } + (true, _) => { + return Err(spanned_error( + "#[positional] can only be used on `Vec`", + span, + )); + } + (false, _) => (), + } + + Ok(()) +} + impl ArgFlag { fn new(name: Ident, short: Option, doc: Vec) -> Self { ArgFlag { @@ -256,20 +335,20 @@ const REQUIRED_FLOATS: [&str; 2] = ["f32", "f64"]; const REQUIRED_INTEGERS: [&str; 12] = [ "i8", "i16", "i32", "i64", "i128", "isize", "u8", "u16", "u32", "u64", "u128", "usize", ]; -const POSITIONAL_PATHS: [&str; 4] = [ +const MULTI_PATHS: [&str; 4] = [ "Vec<::std::path::PathBuf>", "Vec", "Vec", "Vec", ]; -const POSITIONAL_OS_STRINGS: [&str; 4] = [ +const MULTI_OS_STRINGS: [&str; 4] = [ "Vec<::std::ffi::OsString>", "Vec", "Vec", "Vec", ]; -const POSITIONAL_FLOATS: [&str; 2] = ["Vec", "Vec"]; -const POSITIONAL_INTEGERS: [&str; 12] = [ +const MULTI_FLOATS: [&str; 2] = ["Vec", "Vec"]; +const MULTI_INTEGERS: [&str; 12] = [ "Vec", "Vec", "Vec", @@ -312,70 +391,76 @@ const OPTIONAL_INTEGERS: [&str; 12] = [ ]; impl ArgOption { - fn new(name: Ident, short: Option, doc: Vec, path: &str) -> Result { - let optional = if OPTIONAL_PATHS.contains(&path) + fn new( + span: Span, + name: Ident, + short: Option, + doc: Vec, + path: &str, + ) -> Result { + // Parse the argument type and decide what properties it should start with. + let property = if OPTIONAL_PATHS.contains(&path) || OPTIONAL_OS_STRINGS.contains(&path) || OPTIONAL_FLOATS.contains(&path) || OPTIONAL_INTEGERS.contains(&path) || path == "Option" - || POSITIONAL_PATHS.contains(&path) - || POSITIONAL_OS_STRINGS.contains(&path) - || POSITIONAL_FLOATS.contains(&path) - || POSITIONAL_INTEGERS.contains(&path) + { + ArgProperty::Optional + } else if MULTI_PATHS.contains(&path) + || MULTI_OS_STRINGS.contains(&path) + || MULTI_FLOATS.contains(&path) + || MULTI_INTEGERS.contains(&path) || path == "Vec" { - true + ArgProperty::MultiValue { required: false } } else if REQUIRED_PATHS.contains(&path) || REQUIRED_OS_STRINGS.contains(&path) || REQUIRED_FLOATS.contains(&path) || REQUIRED_INTEGERS.contains(&path) || path == "String" { - false + ArgProperty::Required } else { - return Err(()); + return Err(spanned_error( + "Expected bool, PathBuf, String, OsString, integer, or float", + span, + )); }; + // Decide the type to show in the help message. let ty_help = if OPTIONAL_PATHS.contains(&path) || REQUIRED_PATHS.contains(&path) - || POSITIONAL_PATHS.contains(&path) + || MULTI_PATHS.contains(&path) { ArgType::Path } else if OPTIONAL_OS_STRINGS.contains(&path) || REQUIRED_OS_STRINGS.contains(&path) - || POSITIONAL_OS_STRINGS.contains(&path) + || MULTI_OS_STRINGS.contains(&path) { ArgType::OsString } else if path == "String" || path == "Vec" || path == "Option" { ArgType::String } else if OPTIONAL_FLOATS.contains(&path) || REQUIRED_FLOATS.contains(&path) - || POSITIONAL_FLOATS.contains(&path) + || MULTI_FLOATS.contains(&path) { ArgType::Float } else if OPTIONAL_INTEGERS.contains(&path) || REQUIRED_INTEGERS.contains(&path) - || POSITIONAL_INTEGERS.contains(&path) + || MULTI_INTEGERS.contains(&path) { ArgType::Integer } else { unreachable!(); }; - let positional = POSITIONAL_PATHS.contains(&path) - || POSITIONAL_OS_STRINGS.contains(&path) - || POSITIONAL_FLOATS.contains(&path) - || POSITIONAL_INTEGERS.contains(&path) - || path == "Vec"; - Ok(ArgOption { name, short, ty_help, doc, default: None, - optional, - positional, + property, }) } diff --git a/onlyargs_derive/tests/parsing.rs b/onlyargs_derive/tests/parsing.rs new file mode 100644 index 0000000..89ded90 --- /dev/null +++ b/onlyargs_derive/tests/parsing.rs @@ -0,0 +1,86 @@ +use onlyargs::{CliError, OnlyArgs as _}; +use onlyargs_derive::OnlyArgs; +use std::{ffi::OsString, path::PathBuf}; + +#[test] +fn test_multivalue_paths() -> Result<(), CliError> { + #[derive(Debug, OnlyArgs)] + struct Args { + path: Vec, + } + + let args = Args::parse( + [ + "--path", + "/tmp/hello", + "--path", + "/var/run/test.pid", + "--path", + "./foo/bar with spaces/", + ] + .into_iter() + .map(OsString::from) + .collect(), + )?; + + assert_eq!( + args.path, + [ + PathBuf::from("/tmp/hello"), + PathBuf::from("/var/run/test.pid"), + PathBuf::from("./foo/bar with spaces/"), + ] + ); + + Ok(()) +} + +#[test] +fn test_multivalue_with_positional() -> Result<(), CliError> { + #[derive(Debug, OnlyArgs)] + struct Args { + names: Vec, + + #[positional] + rest: Vec, + } + + let args = Args::parse( + ["--names", "Alice", "--names", "Bob", "Carol", "David"] + .into_iter() + .map(OsString::from) + .collect(), + )?; + + assert_eq!(args.names, ["Alice", "Bob"]); + assert_eq!(args.rest, ["Carol", "David"]); + + Ok(()) +} + +#[test] +fn test_required_multivalue() -> Result<(), CliError> { + #[derive(Debug, OnlyArgs)] + struct Args { + #[required] + names: Vec, + } + + // Empty `--names` is not allowed. + assert!(matches!( + Args::parse(vec![]), + Err(CliError::MissingRequired(name)) if name == "--names", + )); + + // At least one `--names` is required. + let args = Args::parse( + ["--names", "Alice"] + .into_iter() + .map(OsString::from) + .collect(), + )?; + + assert_eq!(args.names, ["Alice"]); + + Ok(()) +} diff --git a/src/traits.rs b/src/traits.rs index 27dd685..d4306f9 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -190,3 +190,18 @@ impl RequiredArgExt for Option { self.ok_or_else(|| CliError::MissingRequired(name.into())) } } + +impl RequiredArgExt for Vec { + type Inner = Vec; + + fn required(self, name: N) -> Result + where + N: Into, + { + if self.is_empty() { + Err(CliError::MissingRequired(name.into())) + } else { + Ok(self) + } + } +}