├── .gitignore ├── examples ├── full │ ├── README.md │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── basic │ ├── README.md │ ├── Cargo.toml │ └── src │ │ └── main.rs └── full-derive │ ├── README.md │ ├── Cargo.toml │ └── src │ └── main.rs ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── onlyargs_derive ├── compile_tests │ ├── empty.rs │ ├── multivalue_i8.rs │ ├── multivalue_u8.rs │ ├── struct_doc_comment.rs │ ├── struct_footer.rs │ ├── multivalue_f32.rs │ ├── multivalue_f64.rs │ ├── multivalue_i128.rs │ ├── multivalue_isize.rs │ ├── multivalue_u128.rs │ ├── multivalue_usize.rs │ ├── multivalue_string.rs │ ├── default_i128.rs │ ├── default_i8.rs │ ├── default_u128.rs │ ├── default_u8.rs │ ├── conflicting_short_name.rs │ ├── default_f32.rs │ ├── default_f64.rs │ ├── default_isize.rs │ ├── default_usize.rs │ ├── multivalue_osstring.rs │ ├── multivalue_pathbuf.rs │ ├── positional_f32.rs │ ├── positional_f64.rs │ ├── positional_i8.rs │ ├── positional_u8.rs │ ├── default_bool_false.rs │ ├── default_bool_true.rs │ ├── default_multivalue.rs │ ├── default_negative_i128.rs │ ├── default_negative_i8.rs │ ├── default_string.rs │ ├── positional_i128.rs │ ├── positional_isize.rs │ ├── positional_option.rs │ ├── positional_single_bool.rs │ ├── positional_string.rs │ ├── positional_u128.rs │ ├── positional_usize.rs │ ├── required_bool.rs │ ├── default_negative_isize.rs │ ├── default_option.rs │ ├── positional_single_string.rs │ ├── required_string.rs │ ├── required_option.rs │ ├── default_osstring.rs │ ├── default_pathbuf.rs │ ├── positional_osstring.rs │ ├── positional_pathbuf.rs │ ├── ignore_short_name.rs │ ├── default_positional.rs │ ├── manual_short_name.rs │ ├── positional_option.stderr │ ├── positional_single_bool.stderr │ ├── required_bool.stderr │ ├── conflicting_positional.stderr │ ├── default_multivalue.stderr │ ├── default_positional.stderr │ ├── conflicting_short_name.stderr │ ├── default_option.stderr │ ├── positional_single_string.stderr │ ├── required_string.stderr │ ├── required_option.stderr │ ├── conflicting_positional.rs │ ├── optional.rs │ └── compiler.rs ├── Cargo.toml ├── README.md ├── tests │ └── parsing.rs └── src │ ├── parser.rs │ └── lib.rs ├── Cargo.toml ├── MSRV.md ├── LICENSE ├── README.md ├── src ├── traits.rs └── lib.rs └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /examples/full/README.md: -------------------------------------------------------------------------------- 1 | Run with 2 | ``` 3 | cargo run -p example-full 4 | ``` 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: 2 | - parasyte 3 | ko_fi: blipjoy 4 | patreon: blipjoy 5 | -------------------------------------------------------------------------------- /examples/basic/README.md: -------------------------------------------------------------------------------- 1 | Run with 2 | ``` 3 | cargo run -p example-basic 4 | ``` 5 | -------------------------------------------------------------------------------- /examples/full-derive/README.md: -------------------------------------------------------------------------------- 1 | Run with 2 | ``` 3 | cargo run -p example-full-derive 4 | ``` 5 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/empty.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args {} 3 | 4 | fn main() {} 5 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/multivalue_i8.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | vertices: Vec, 4 | } 5 | 6 | fn main() {} 7 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/multivalue_u8.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | vertices: Vec, 4 | } 5 | 6 | fn main() {} 7 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/struct_doc_comment.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | /// Doc comment message 3 | struct Args {} 4 | 5 | fn main() {} 6 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/struct_footer.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | #[footer = "Footer message"] 3 | struct Args {} 4 | 5 | fn main() {} 6 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/multivalue_f32.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | vertices: Vec, 4 | } 5 | 6 | fn main() {} 7 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/multivalue_f64.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | vertices: Vec, 4 | } 5 | 6 | fn main() {} 7 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/multivalue_i128.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | vertices: Vec, 4 | } 5 | 6 | fn main() {} 7 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/multivalue_isize.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | vertices: Vec, 4 | } 5 | 6 | fn main() {} 7 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/multivalue_u128.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | vertices: Vec, 4 | } 5 | 6 | fn main() {} 7 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/multivalue_usize.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | vertices: Vec, 4 | } 5 | 6 | fn main() {} 7 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/multivalue_string.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | strings: Vec, 4 | } 5 | 6 | fn main() {} 7 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/default_i128.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[default(42)] 4 | width: i128, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/default_i8.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[default(42)] 4 | width: i8, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/default_u128.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[default(42)] 4 | width: u128, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/default_u8.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[default(42)] 4 | width: u8, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/conflicting_short_name.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | min: i32, 4 | max: i32, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/default_f32.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[default(42.123)] 4 | width: f32, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/default_f64.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[default(42.123)] 4 | width: f64, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/default_isize.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[default(42)] 4 | width: isize, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/default_usize.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[default(42)] 4 | width: usize, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/multivalue_osstring.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | strings: Vec, 4 | } 5 | 6 | fn main() {} 7 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/multivalue_pathbuf.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | paths: Vec, 4 | } 5 | 6 | fn main() {} 7 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/positional_f32.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[positional] 4 | rest: Vec, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/positional_f64.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[positional] 4 | rest: Vec, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/positional_i8.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[positional] 4 | rest: Vec, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/positional_u8.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[positional] 4 | rest: Vec, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/default_bool_false.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[default(false)] 4 | maybe: bool, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/default_bool_true.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[default(true)] 4 | maybe: bool, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/default_multivalue.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[default(123)] 4 | nums: Vec, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/default_negative_i128.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[default(-42)] 4 | width: i128, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/default_negative_i8.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[default(-42)] 4 | width: i8, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/default_string.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[default("foo bar")] 4 | name: String, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/positional_i128.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[positional] 4 | rest: Vec, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/positional_isize.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[positional] 4 | rest: Vec, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/positional_option.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[positional] 4 | rest: Option, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/positional_single_bool.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[positional] 4 | rest: bool, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/positional_string.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[positional] 4 | rest: Vec, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/positional_u128.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[positional] 4 | rest: Vec, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/positional_usize.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[positional] 4 | rest: Vec, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/required_bool.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[required] 4 | required_bool: bool, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/default_negative_isize.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[default(-42)] 4 | width: isize, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/default_option.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[default(123)] 4 | opt_num: Option, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/positional_single_string.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[positional] 4 | rest: String, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/required_string.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[required] 4 | required_string: String, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/required_option.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[required] 4 | required_option: Option, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/default_osstring.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[default("foo bar")] 4 | name: std::ffi::OsString, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/default_pathbuf.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[default("./foo/bar")] 4 | path: std::path::PathBuf, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/positional_osstring.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[positional] 4 | rest: Vec, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/positional_pathbuf.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[positional] 4 | rest: Vec, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/ignore_short_name.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | min: i32, 4 | 5 | #[long] 6 | max: i32, 7 | } 8 | 9 | fn main() {} 10 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/default_positional.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[positional] 4 | #[default(123)] 5 | nums: Vec, 6 | } 7 | 8 | fn main() {} 9 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/manual_short_name.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | min: i32, 4 | 5 | #[short('x')] 6 | max: i32, 7 | } 8 | 9 | fn main() {} 10 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/positional_option.stderr: -------------------------------------------------------------------------------- 1 | error: #[positional] can only be used on `Vec` 2 | --> compile_tests/positional_option.rs:4:11 3 | | 4 | 4 | rest: Option, 5 | | ^^^^^^ 6 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/positional_single_bool.stderr: -------------------------------------------------------------------------------- 1 | error: #[positional] can only be used on `Vec` 2 | --> compile_tests/positional_single_bool.rs:4:11 3 | | 4 | 4 | rest: bool, 5 | | ^^^^ 6 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/required_bool.stderr: -------------------------------------------------------------------------------- 1 | error: #[required] can only be used on `Vec` 2 | --> compile_tests/required_bool.rs:4:20 3 | | 4 | 4 | required_bool: bool, 5 | | ^^^^ 6 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/conflicting_positional.stderr: -------------------------------------------------------------------------------- 1 | error: Positional arguments can only be specified once. 2 | --> compile_tests/conflicting_positional.rs:6:5 3 | | 4 | 6 | more: Vec, 5 | | ^^^^ 6 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/default_multivalue.stderr: -------------------------------------------------------------------------------- 1 | error: #[default(...)] can only be used on primitive types 2 | --> compile_tests/default_multivalue.rs:4:11 3 | | 4 | 4 | nums: Vec, 5 | | ^^^ 6 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/default_positional.stderr: -------------------------------------------------------------------------------- 1 | error: #[default(...)] can only be used on primitive types 2 | --> compile_tests/default_positional.rs:5:11 3 | | 4 | 5 | nums: Vec, 5 | | ^^^ 6 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/conflicting_short_name.stderr: -------------------------------------------------------------------------------- 1 | error: Only one short arg is allowed. `-m` also used on field `min` 2 | --> compile_tests/conflicting_short_name.rs:4:5 3 | | 4 | 4 | max: i32, 5 | | ^^^ 6 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/default_option.stderr: -------------------------------------------------------------------------------- 1 | error: #[default(...)] can only be used on primitive types 2 | --> compile_tests/default_option.rs:4:14 3 | | 4 | 4 | opt_num: Option, 5 | | ^^^^^^ 6 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/positional_single_string.stderr: -------------------------------------------------------------------------------- 1 | error: #[positional] can only be used on `Vec` 2 | --> compile_tests/positional_single_string.rs:4:11 3 | | 4 | 4 | rest: String, 5 | | ^^^^^^ 6 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/required_string.stderr: -------------------------------------------------------------------------------- 1 | error: #[required] can only be used on `Vec` 2 | --> compile_tests/required_string.rs:4:22 3 | | 4 | 4 | required_string: String, 5 | | ^^^^^^ 6 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/required_option.stderr: -------------------------------------------------------------------------------- 1 | error: #[required] can only be used on `Vec` 2 | --> compile_tests/required_option.rs:4:22 3 | | 4 | 4 | required_option: Option, 5 | | ^^^^^^ 6 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/conflicting_positional.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[positional] 4 | rest: Vec, 5 | #[positional] 6 | more: Vec, 7 | } 8 | 9 | fn main() {} 10 | -------------------------------------------------------------------------------- /examples/basic/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-basic" 3 | description = "Basic example application for onlyargs." 4 | version = "0.1.0" 5 | edition = "2021" 6 | publish = false 7 | 8 | [dependencies] 9 | error-iter = "0.4" 10 | onlyargs = { path = "../.." } 11 | -------------------------------------------------------------------------------- /examples/full/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-full" 3 | description = "Example application for onlyargs." 4 | version = "0.1.0" 5 | edition = "2021" 6 | publish = false 7 | 8 | [dependencies] 9 | error-iter = "0.4" 10 | onlyargs = { path = "../.." } 11 | onlyerror = "0.1" 12 | -------------------------------------------------------------------------------- /examples/full-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-full-derive" 3 | description = "Example application for onlyargs_derive." 4 | version = "0.1.0" 5 | edition = "2021" 6 | publish = false 7 | 8 | [dependencies] 9 | error-iter = "0.4" 10 | onlyargs = { path = "../.." } 11 | onlyargs_derive = { path = "../../onlyargs_derive" } 12 | onlyerror = "0.1" 13 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "onlyargs" 3 | description = "Obsessively tiny argument parsing" 4 | version = "0.2.0" 5 | authors = ["Jay Oster "] 6 | repository = "https://github.com/parasyte/onlyargs" 7 | edition = "2021" 8 | rust-version = "1.62.0" 9 | keywords = ["cli", "arg", "argument", "parse", "parser"] 10 | categories = ["command-line-interface"] 11 | license = "MIT" 12 | exclude = [ 13 | "/.github", 14 | "/MSRV.md", 15 | ] 16 | 17 | [dependencies] 18 | # No dependencies! 19 | 20 | [dev-dependencies] 21 | error-iter = "0.4" 22 | 23 | [workspace] 24 | members = [ 25 | "examples/*", 26 | "onlyargs_derive", 27 | ] 28 | -------------------------------------------------------------------------------- /onlyargs_derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "onlyargs_derive" 3 | description = "Obsessively tiny argument parsing derive macro" 4 | version = "0.2.0" 5 | authors = ["Jay Oster "] 6 | repository = "https://github.com/parasyte/onlyargs" 7 | edition = "2021" 8 | rust-version = "1.62.0" 9 | keywords = ["cli", "arg", "argument", "parse", "parser"] 10 | categories = ["command-line-interface"] 11 | license = "MIT" 12 | exclude = [ 13 | "/compile_tests", 14 | "/tests", 15 | ] 16 | 17 | [lib] 18 | proc-macro = true 19 | 20 | [[test]] 21 | name = "compile_and_fail" 22 | path = "compile_tests/compiler.rs" 23 | 24 | [dependencies] 25 | myn = "0.2.1" 26 | onlyargs = { version = "0.2", path = ".." } 27 | 28 | [dev-dependencies] 29 | trybuild = "1" 30 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/optional.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, onlyargs_derive::OnlyArgs)] 2 | struct Args { 3 | #[long] 4 | opt_f32: Option, 5 | #[long] 6 | opt_f64: Option, 7 | #[long] 8 | opt_i8: Option, 9 | #[long] 10 | opt_i16: Option, 11 | #[long] 12 | opt_i32: Option, 13 | #[long] 14 | opt_i64: Option, 15 | #[long] 16 | opt_i128: Option, 17 | #[long] 18 | opt_isize: Option, 19 | #[long] 20 | opt_u8: Option, 21 | #[long] 22 | opt_u16: Option, 23 | #[long] 24 | opt_u32: Option, 25 | #[long] 26 | opt_u64: Option, 27 | #[long] 28 | opt_u128: Option, 29 | #[long] 30 | opt_usize: Option, 31 | #[long] 32 | opt_osstring: Option, 33 | #[long] 34 | opt_path: Option, 35 | #[long] 36 | opt_string: Option, 37 | } 38 | 39 | fn main() {} 40 | -------------------------------------------------------------------------------- /MSRV.md: -------------------------------------------------------------------------------- 1 | # Minimum Supported Rust Version 2 | 3 | | `onlyargs` version | `rustc` version | 4 | |--------------------|-----------------| 5 | | (unreleased) | `1.62.0` | 6 | | `0.2.0` | `1.62.0` | 7 | | `0.1.3` | `1.62.0` | 8 | | `0.1.2` | `1.62.0` | 9 | | `0.1.1` | `1.62.0` | 10 | | `0.1.0` | `1.62.0` | 11 | 12 | ## Policy 13 | 14 | The table above will be kept up-to-date in lock-step with CI on the main branch in GitHub. It may contain information about unreleased and yanked versions. It is the user's responsibility to consult with the [`onlyargs` versions page](https://crates.io/crates/onlyargs/versions) on `crates.io` to verify version status. 15 | 16 | The MSRV will be chosen as the minimum version of `rustc` that can successfully pass CI, including documentation, lints, and all examples. For this reason, the minimum version _supported_ may be higher than the minimum version _required_ to compile the `onlyargs` crate itself. See `Cargo.toml` for the minimal Rust version required to build the crate alone. 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Jay Oster 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /onlyargs_derive/README.md: -------------------------------------------------------------------------------- 1 | [![Crates.io](https://img.shields.io/crates/v/onlyargs_derive)](https://crates.io/crates/onlyargs_derive "Crates.io version") 2 | [![Documentation](https://img.shields.io/docsrs/onlyargs_derive)](https://docs.rs/onlyargs_derive "Documentation") 3 | [![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/) 4 | [![GitHub actions](https://img.shields.io/github/actions/workflow/status/parasyte/onlyargs/ci.yml?branch=main)](https://github.com/parasyte/onlyargs/actions "CI") 5 | [![GitHub activity](https://img.shields.io/github/last-commit/parasyte/onlyargs)](https://github.com/parasyte/onlyargs/commits "Commit activity") 6 | [![GitHub Sponsors](https://img.shields.io/github/sponsors/parasyte)](https://github.com/sponsors/parasyte "Sponsors") 7 | 8 | Only argument parsing! Nothing more. 9 | 10 | # Why? 11 | 12 | - 100% safe Rust 🦀. 13 | - Correctness: Paths with invalid UTF-8 work correctly on all platforms. 14 | - Fast compile times. 15 | - See [`myn` benchmark results](https://github.com/parasyte/myn/blob/main/benchmarks.md). 16 | - Convenience: `#[derive(OnlyArgs)]` on a struct and parse CLI arguments from the environment into it with minimal boilerplate. 17 | 18 | ## MSRV Policy 19 | 20 | The Minimum Supported Rust Version for `onlyargs` will always be made available in the [MSRV.md](../MSRV.md) file on GitHub. 21 | 22 | # Examples 23 | 24 | See the [`derive-example`](../examples/full-derive/src/main.rs) for usage. 25 | -------------------------------------------------------------------------------- /examples/basic/src/main.rs: -------------------------------------------------------------------------------- 1 | use error_iter::ErrorIter as _; 2 | use onlyargs::{CliError, OnlyArgs}; 3 | use std::{ffi::OsString, process::ExitCode}; 4 | 5 | #[derive(Debug)] 6 | struct Args { 7 | verbose: bool, 8 | } 9 | 10 | impl OnlyArgs for Args { 11 | const HELP: &'static str = onlyargs::impl_help!(); 12 | const VERSION: &'static str = onlyargs::impl_version!(); 13 | 14 | fn parse(args: Vec) -> Result { 15 | let mut verbose = false; 16 | 17 | for arg in args.into_iter() { 18 | match arg.to_str() { 19 | Some("--help") | Some("-h") => Self::help(), 20 | Some("--version") | Some("-V") => Self::version(), 21 | Some("--verbose") | Some("-v") => { 22 | verbose = true; 23 | } 24 | Some("--") => break, 25 | _ => return Err(CliError::Unknown(arg)), 26 | } 27 | } 28 | 29 | Ok(Self { verbose }) 30 | } 31 | } 32 | 33 | fn run() -> Result<(), CliError> { 34 | let args: Args = onlyargs::parse()?; 35 | 36 | println!("Arguments parsed successfully!"); 37 | 38 | if args.verbose { 39 | println!("Verbose output is enabled"); 40 | } 41 | 42 | Ok(()) 43 | } 44 | 45 | fn main() -> ExitCode { 46 | match run() { 47 | Ok(_) => ExitCode::SUCCESS, 48 | Err(err) => { 49 | eprintln!("{}", Args::HELP); 50 | 51 | eprintln!("Error: {err}"); 52 | for source in err.sources().skip(1) { 53 | eprintln!(" Caused by: {source}"); 54 | } 55 | 56 | ExitCode::FAILURE 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | pull_request: 5 | schedule: 6 | - cron: '0 0 1 * *' 7 | jobs: 8 | checks: 9 | name: Check 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | rust: 14 | - stable 15 | - beta 16 | - 1.62.0 17 | steps: 18 | - name: Checkout sources 19 | uses: actions/checkout@v4 20 | - name: Install toolchain 21 | uses: dtolnay/rust-toolchain@master 22 | with: 23 | toolchain: ${{ matrix.rust }} 24 | - name: Rust cache 25 | uses: Swatinem/rust-cache@v2 26 | with: 27 | shared-key: common 28 | - name: Cargo check 29 | run: cargo check --workspace 30 | 31 | lints: 32 | name: Lints 33 | runs-on: ubuntu-latest 34 | steps: 35 | - name: Checkout sources 36 | uses: actions/checkout@v4 37 | - name: Install toolchain 38 | uses: dtolnay/rust-toolchain@master 39 | with: 40 | toolchain: stable 41 | components: clippy, rustfmt 42 | - name: Rust cache 43 | uses: Swatinem/rust-cache@v2 44 | with: 45 | shared-key: common 46 | - name: Install cargo-machete 47 | uses: baptiste0928/cargo-install@v3 48 | with: 49 | crate: cargo-machete 50 | - name: Cargo fmt 51 | run: cargo fmt --all -- --check 52 | - name: Cargo doc 53 | run: cargo doc --workspace --no-deps 54 | - name: Cargo clippy 55 | run: cargo clippy --workspace --tests -- -D warnings 56 | - name: Cargo machete 57 | run: cargo machete 58 | 59 | tests: 60 | name: Test 61 | runs-on: ubuntu-latest 62 | needs: [checks, lints] 63 | strategy: 64 | matrix: 65 | rust: 66 | - stable 67 | - beta 68 | - 1.62.0 69 | steps: 70 | - name: Checkout sources 71 | uses: actions/checkout@v4 72 | - name: Install toolchain 73 | uses: dtolnay/rust-toolchain@master 74 | with: 75 | toolchain: ${{ matrix.rust }} 76 | - name: Rust cache 77 | uses: Swatinem/rust-cache@v2 78 | with: 79 | shared-key: common 80 | - name: Cargo test 81 | run: cargo test --workspace 82 | -------------------------------------------------------------------------------- /examples/full-derive/src/main.rs: -------------------------------------------------------------------------------- 1 | //! This example is functionally identical to the `onlyargs` "full" example. 2 | //! 3 | //! It shows that if you can use `derive` macros, a lot of boilerplate can be scrubbed away! 4 | 5 | use error_iter::ErrorIter as _; 6 | use onlyargs::{CliError, OnlyArgs as _}; 7 | use onlyargs_derive::OnlyArgs; 8 | use onlyerror::Error; 9 | use std::{fmt::Write as _, path::PathBuf, process::ExitCode}; 10 | 11 | /// A basic argument parsing example with `onlyargs_derive`. 12 | /// Sums a list of numbers and writes the result to a file or standard output. 13 | #[derive(Clone, Debug, Eq, PartialEq, OnlyArgs)] 14 | #[footer = "Please consider becoming a sponsor 💖:"] 15 | #[footer = " * https://github.com/sponsors/parasyte"] 16 | #[footer = " * https://ko-fi.com/blipjoy"] 17 | #[footer = " * https://patreon.com/blipjoy"] 18 | struct Args { 19 | /// Your username. 20 | username: String, 21 | 22 | /// Output file path. 23 | output: Option, 24 | 25 | /// A list of numbers to sum. 26 | numbers: Vec, 27 | 28 | /// Set the width. 29 | #[default(42)] 30 | width: i32, 31 | 32 | /// Enable verbose output. 33 | verbose: bool, 34 | } 35 | 36 | #[derive(Debug, Error)] 37 | enum Error { 38 | /// Argument parsing error. 39 | Cli(#[from] CliError), 40 | 41 | /// I/O error. 42 | Io(#[from] std::io::Error), 43 | } 44 | 45 | fn run() -> Result<(), Error> { 46 | let args: Args = onlyargs::parse()?; 47 | 48 | println!("Hello, {}!", args.username); 49 | println!("The width is {}.", args.width); 50 | 51 | // Do some work. 52 | let numbers = &args.numbers.iter().fold(String::new(), |mut numbers, num| { 53 | write!(numbers, " + {num}").unwrap(); 54 | numbers 55 | }); 56 | 57 | if let Some(numbers) = numbers.strip_prefix(" + ") { 58 | let sum: i32 = args.numbers.iter().sum(); 59 | let msg = format!("The sum of {numbers} is {sum}"); 60 | 61 | if let Some(path) = &args.output { 62 | std::fs::write(path, msg + "\n")?; 63 | println!("Sums written to {path:?}"); 64 | } else { 65 | println!("{msg}"); 66 | } 67 | } 68 | 69 | // And finally some debug info. 70 | if args.verbose { 71 | println!(); 72 | dbg!(args); 73 | } 74 | 75 | Ok(()) 76 | } 77 | 78 | fn main() -> ExitCode { 79 | match run() { 80 | Ok(_) => ExitCode::SUCCESS, 81 | Err(err) => { 82 | if matches!(err, Error::Cli(_)) { 83 | eprintln!("{}", Args::HELP); 84 | } 85 | 86 | eprintln!("Error: {err}"); 87 | for source in err.sources().skip(1) { 88 | eprintln!(" Caused by: {source}"); 89 | } 90 | 91 | ExitCode::FAILURE 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /onlyargs_derive/compile_tests/compiler.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn compile_tests() { 3 | let t = trybuild::TestCases::new(); 4 | t.pass("compile_tests/default_bool_false.rs"); 5 | t.pass("compile_tests/default_bool_true.rs"); 6 | t.pass("compile_tests/default_f32.rs"); 7 | t.pass("compile_tests/default_f64.rs"); 8 | t.pass("compile_tests/default_i8.rs"); 9 | t.pass("compile_tests/default_i128.rs"); 10 | t.pass("compile_tests/default_isize.rs"); 11 | // TODO: Negatives are not supported yet! 12 | // t.pass("compile_tests/default_negative_i8.rs"); 13 | // t.pass("compile_tests/default_negative_i128.rs"); 14 | // t.pass("compile_tests/default_negative_isize.rs"); 15 | t.pass("compile_tests/default_osstring.rs"); 16 | t.pass("compile_tests/default_pathbuf.rs"); 17 | t.pass("compile_tests/default_string.rs"); 18 | t.pass("compile_tests/default_u8.rs"); 19 | t.pass("compile_tests/default_u128.rs"); 20 | t.pass("compile_tests/default_usize.rs"); 21 | 22 | t.pass("compile_tests/positional_f32.rs"); 23 | t.pass("compile_tests/positional_f64.rs"); 24 | t.pass("compile_tests/positional_i8.rs"); 25 | t.pass("compile_tests/positional_i128.rs"); 26 | t.pass("compile_tests/positional_isize.rs"); 27 | t.pass("compile_tests/positional_osstring.rs"); 28 | t.pass("compile_tests/positional_pathbuf.rs"); 29 | t.pass("compile_tests/positional_string.rs"); 30 | t.pass("compile_tests/positional_u8.rs"); 31 | t.pass("compile_tests/positional_u128.rs"); 32 | t.pass("compile_tests/positional_usize.rs"); 33 | t.compile_fail("compile_tests/conflicting_positional.rs"); 34 | 35 | t.pass("compile_tests/multivalue_f32.rs"); 36 | t.pass("compile_tests/multivalue_f64.rs"); 37 | t.pass("compile_tests/multivalue_i8.rs"); 38 | t.pass("compile_tests/multivalue_i128.rs"); 39 | t.pass("compile_tests/multivalue_isize.rs"); 40 | t.pass("compile_tests/multivalue_u8.rs"); 41 | t.pass("compile_tests/multivalue_u128.rs"); 42 | t.pass("compile_tests/multivalue_usize.rs"); 43 | t.pass("compile_tests/multivalue_osstring.rs"); 44 | t.pass("compile_tests/multivalue_pathbuf.rs"); 45 | t.pass("compile_tests/multivalue_string.rs"); 46 | 47 | t.pass("compile_tests/empty.rs"); 48 | t.pass("compile_tests/optional.rs"); 49 | t.pass("compile_tests/struct_doc_comment.rs"); 50 | t.pass("compile_tests/struct_footer.rs"); 51 | 52 | t.compile_fail("compile_tests/conflicting_short_name.rs"); 53 | t.pass("compile_tests/manual_short_name.rs"); 54 | t.pass("compile_tests/ignore_short_name.rs"); 55 | 56 | // Various expected errors. 57 | t.compile_fail("compile_tests/required_bool.rs"); 58 | t.compile_fail("compile_tests/required_option.rs"); 59 | t.compile_fail("compile_tests/required_string.rs"); 60 | t.compile_fail("compile_tests/default_multivalue.rs"); 61 | t.compile_fail("compile_tests/default_option.rs"); 62 | t.compile_fail("compile_tests/default_positional.rs"); 63 | t.compile_fail("compile_tests/positional_option.rs"); 64 | t.compile_fail("compile_tests/positional_single_bool.rs"); 65 | t.compile_fail("compile_tests/positional_single_string.rs"); 66 | } 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Crates.io](https://img.shields.io/crates/v/onlyargs)](https://crates.io/crates/onlyargs "Crates.io version") 2 | [![Documentation](https://img.shields.io/docsrs/onlyargs)](https://docs.rs/onlyargs "Documentation") 3 | [![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/) 4 | [![GitHub actions](https://img.shields.io/github/actions/workflow/status/parasyte/onlyargs/ci.yml?branch=main)](https://github.com/parasyte/onlyargs/actions "CI") 5 | [![GitHub activity](https://img.shields.io/github/last-commit/parasyte/onlyargs)](https://github.com/parasyte/onlyargs/commits "Commit activity") 6 | [![GitHub Sponsors](https://img.shields.io/github/sponsors/parasyte)](https://github.com/sponsors/parasyte "Sponsors") 7 | 8 | Only argument parsing! Nothing more. 9 | 10 | # Why? 11 | 12 | - 100% safe Rust 🦀. 13 | - Correctness: Paths with invalid UTF-8 work correctly on all platforms. 14 | - Fast compile times. 15 | - Convenience: `#[derive(OnlyArgs)]` on a struct and parse CLI arguments from the environment into it with minimal boilerplate. 16 | 17 | ## MSRV Policy 18 | 19 | The Minimum Supported Rust Version for `onlyargs` will always be made available in the [MSRV.md](./MSRV.md) file on GitHub. 20 | 21 | 22 | # Rationale 23 | 24 | There's an [argument parsing crate for everyone](https://github.com/rosetta-rs/argparse-rosetta-rs). So why write another? 25 | 26 | `onlyargs` is an example of extreme minimalism! The only thing it provides is a trait and some utility functions; you're expected to do the actual work to implement it for your CLI argument struct. But don't let that scare you away! The parser implementation in the ["full" example](./examples/full/src/main.rs) is only around 50 lines! (Most of the file is boilerplate.) 27 | 28 | The goals of this parser are correctness, fast compile times, and convenience. 29 | 30 | ## 100% safe Rust 31 | 32 | No shenanigans! The only `unsafe` code is abstracted away in the standard library. 33 | 34 | ## Correctness 35 | 36 | - The main parsing loop uses `OsString` so that invalid UTF-8 can be accepted as an argument. 37 | - Arguments can either be stored directly as an `OsString` or converted to a `PathBuf` with no extra cost. Easily access your [mojibake](https://en.wikipedia.org/wiki/Mojibake) file systems! 38 | - Conversions from `OsString` are handled by your parser implementation. It's only as correct as you want it to be! 39 | - Play with the examples. Try to break it. Have fun! 40 | 41 | ## Fast compile times 42 | 43 | See [`myn` benchmark results](https://github.com/parasyte/myn/blob/main/benchmarks.md). 44 | 45 | ## Convenience 46 | 47 | Argument parsing is dead simple (assuming your preferred DSL is opinionated and no-nonsense). There is no reason to overcomplicate it by supporting multiple forms like `--argument 123` and `--argument=123` or `-a 123` and `-a123`. _Just pick one!_ 48 | 49 | The provided examples use the former in both cases: `--argument 123` and `-a 123` are accepted for arguments with a value. Supporting both long and short argument names is just a pattern! 50 | 51 | ```rust 52 | Some("--argument") | Some("-a") 53 | ``` 54 | 55 | It is fairly straightforward to derive an implementation with a proc_macro. Compare the ["full-derive" example](./examples/full-derive/src/main.rs) to the "full" example. 56 | -------------------------------------------------------------------------------- /onlyargs_derive/tests/parsing.rs: -------------------------------------------------------------------------------- 1 | use onlyargs::{CliError, OnlyArgs as _}; 2 | use onlyargs_derive::OnlyArgs; 3 | use std::{ffi::OsString, path::PathBuf}; 4 | 5 | #[test] 6 | fn test_multivalue_paths() -> Result<(), CliError> { 7 | #[derive(Debug, OnlyArgs)] 8 | struct Args { 9 | path: Vec, 10 | } 11 | 12 | let args = Args::parse( 13 | [ 14 | "--path", 15 | "/tmp/hello", 16 | "--path", 17 | "/var/run/test.pid", 18 | "--path", 19 | "./foo/bar with spaces/", 20 | ] 21 | .into_iter() 22 | .map(OsString::from) 23 | .collect(), 24 | )?; 25 | 26 | assert_eq!( 27 | args.path, 28 | [ 29 | PathBuf::from("/tmp/hello"), 30 | PathBuf::from("/var/run/test.pid"), 31 | PathBuf::from("./foo/bar with spaces/"), 32 | ] 33 | ); 34 | 35 | Ok(()) 36 | } 37 | 38 | #[test] 39 | fn test_multivalue_with_positional() -> Result<(), CliError> { 40 | #[derive(Debug, OnlyArgs)] 41 | struct Args { 42 | names: Vec, 43 | 44 | #[positional] 45 | rest: Vec, 46 | } 47 | 48 | let args = Args::parse( 49 | ["--names", "Alice", "--names", "Bob", "Carol", "David"] 50 | .into_iter() 51 | .map(OsString::from) 52 | .collect(), 53 | )?; 54 | 55 | assert_eq!(args.names, ["Alice", "Bob"]); 56 | assert_eq!(args.rest, ["Carol", "David"]); 57 | 58 | Ok(()) 59 | } 60 | 61 | #[test] 62 | fn test_required_multivalue() -> Result<(), CliError> { 63 | #[derive(Debug, OnlyArgs)] 64 | struct Args { 65 | #[required] 66 | names: Vec, 67 | } 68 | 69 | // Empty `--names` is not allowed. 70 | assert!(matches!( 71 | Args::parse(vec![]), 72 | Err(CliError::MissingRequired(name)) if name == "--names", 73 | )); 74 | 75 | // At least one `--names` is required. 76 | let args = Args::parse( 77 | ["--names", "Alice"] 78 | .into_iter() 79 | .map(OsString::from) 80 | .collect(), 81 | )?; 82 | 83 | assert_eq!(args.names, ["Alice"]); 84 | 85 | Ok(()) 86 | } 87 | 88 | #[test] 89 | fn test_required_positional() -> Result<(), CliError> { 90 | #[derive(Debug, OnlyArgs)] 91 | struct Args { 92 | #[required] 93 | #[positional] 94 | rest: Vec, 95 | } 96 | 97 | // Empty positional is not allowed. 98 | assert!(matches!( 99 | dbg!(Args::parse(vec![])), 100 | Err(CliError::MissingRequired(name)) if name == "rest", 101 | )); 102 | 103 | // At least one positional is required. 104 | let args = Args::parse(["Bob"].into_iter().map(OsString::from).collect())?; 105 | 106 | assert_eq!(args.rest, ["Bob"]); 107 | 108 | Ok(()) 109 | } 110 | 111 | #[test] 112 | fn test_positional_escape() -> Result<(), CliError> { 113 | #[derive(Debug, OnlyArgs)] 114 | struct Args { 115 | opt_str: Option, 116 | 117 | #[positional] 118 | rest: Vec, 119 | } 120 | 121 | // All args are optional. 122 | let args = Args::parse(vec![])?; 123 | 124 | assert_eq!(args.opt_str, None); 125 | assert!(args.rest.is_empty()); 126 | 127 | // Captures positional args. 128 | let args = Args::parse( 129 | ["Alice", "--name", "Bob"] 130 | .into_iter() 131 | .map(OsString::from) 132 | .collect(), 133 | )?; 134 | 135 | assert_eq!(args.opt_str, None); 136 | assert_eq!(args.rest, ["Alice", "--name", "Bob"]); 137 | 138 | // Captures the optional string anywhere... 139 | let args = Args::parse( 140 | ["Alice", "--opt-str", "--name", "Bob"] 141 | .into_iter() 142 | .map(OsString::from) 143 | .collect(), 144 | )?; 145 | 146 | assert_eq!(args.opt_str, Some("--name".to_string())); 147 | assert_eq!(args.rest, ["Alice", "Bob"]); 148 | 149 | // ... Unless the `--` escape sequence is encountered. 150 | let args = Args::parse( 151 | ["Alice", "--", "--opt-str", "--name", "Bob"] 152 | .into_iter() 153 | .map(OsString::from) 154 | .collect(), 155 | )?; 156 | 157 | assert_eq!(args.opt_str, None); 158 | assert_eq!(args.rest, ["Alice", "--opt-str", "--name", "Bob"]); 159 | 160 | Ok(()) 161 | } 162 | -------------------------------------------------------------------------------- /examples/full/src/main.rs: -------------------------------------------------------------------------------- 1 | use error_iter::ErrorIter as _; 2 | use onlyargs::{traits::*, CliError, OnlyArgs}; 3 | use onlyerror::Error; 4 | use std::{ffi::OsString, fmt::Write as _, path::PathBuf, process::ExitCode}; 5 | 6 | #[derive(Debug)] 7 | struct Args { 8 | username: String, 9 | output: Option, 10 | numbers: Vec, 11 | width: i32, 12 | verbose: bool, 13 | } 14 | 15 | impl OnlyArgs for Args { 16 | const HELP: &'static str = concat!( 17 | env!("CARGO_PKG_NAME"), 18 | " v", 19 | env!("CARGO_PKG_VERSION"), 20 | "\n", 21 | env!("CARGO_PKG_DESCRIPTION"), 22 | "\n\n", 23 | "A basic argument parsing example with `onlyargs`.\n", 24 | "Sums a list of numbers and writes the result to a file or standard output.\n", 25 | "\nUsage:\n cargo run -p example-full -- [flags] [options] [numbers...]\n", 26 | "\nFlags:\n", 27 | " -h --help Show this help message.\n", 28 | " -V --version Show the application version.\n", 29 | " -v --verbose Enable verbose output.\n", 30 | "\nOptions:\n", 31 | " -u --username STRING Your username. [required]\n", 32 | " -o --output PATH Output file path.\n", 33 | " -w --width NUMBER Set the width. [default: 42]\n", 34 | "\nNumbers:\n", 35 | " A list of numbers to sum.\n", 36 | "\nPlease consider becoming a sponsor 💖:\n", 37 | " * https://github.com/sponsors/parasyte\n", 38 | " * https://ko-fi.com/blipjoy\n", 39 | " * https://patreon.com/blipjoy\n", 40 | ); 41 | 42 | const VERSION: &'static str = onlyargs::impl_version!(); 43 | 44 | fn parse(args: Vec) -> Result { 45 | let mut username = None; 46 | let mut output = None; 47 | let mut numbers = vec![]; 48 | let mut width = 42; 49 | let mut verbose = false; 50 | 51 | let mut args = args.into_iter(); 52 | while let Some(arg) = args.next() { 53 | match arg.to_str() { 54 | Some("--help") | Some("-h") => Self::help(), 55 | Some("--version") | Some("-V") => Self::version(), 56 | Some(name @ "--username") | Some(name @ "-u") => { 57 | username = Some(args.next().parse_str(name)?); 58 | } 59 | Some(name @ "--output") | Some(name @ "-o") => { 60 | output = Some(args.next().parse_path(name)?); 61 | } 62 | Some(name @ "--width") | Some(name @ "-w") => { 63 | width = args.next().parse_int(name)?; 64 | } 65 | Some("--verbose") | Some("-v") => { 66 | verbose = true; 67 | } 68 | Some("--") => { 69 | // Parse all positional arguments as i32. 70 | for arg in args { 71 | numbers.push(arg.parse_int("")?); 72 | } 73 | break; 74 | } 75 | Some(_) => { 76 | numbers.push(arg.parse_int("")?); 77 | } 78 | None => return Err(onlyargs::CliError::Unknown(arg)), 79 | } 80 | } 81 | 82 | Ok(Self { 83 | username: username.required("--username")?, 84 | output, 85 | numbers, 86 | width, 87 | verbose, 88 | }) 89 | } 90 | } 91 | 92 | #[derive(Debug, Error)] 93 | enum Error { 94 | /// Argument parsing error. 95 | Cli(#[from] CliError), 96 | 97 | /// I/O error. 98 | Io(#[from] std::io::Error), 99 | } 100 | 101 | fn run() -> Result<(), Error> { 102 | let args: Args = onlyargs::parse()?; 103 | 104 | println!("Hello, {}!", args.username); 105 | println!("The width is {}.", args.width); 106 | 107 | // Do some work. 108 | let numbers = &args.numbers.iter().fold(String::new(), |mut numbers, num| { 109 | write!(numbers, " + {num}").unwrap(); 110 | numbers 111 | }); 112 | 113 | if let Some(numbers) = numbers.strip_prefix(" + ") { 114 | let sum: i32 = args.numbers.iter().sum(); 115 | let msg = format!("The sum of {numbers} is {sum}"); 116 | 117 | if let Some(path) = &args.output { 118 | std::fs::write(path, msg + "\n")?; 119 | println!("Sums written to {path:?}"); 120 | } else { 121 | println!("{msg}"); 122 | } 123 | } 124 | 125 | // And finally some debug info. 126 | if args.verbose { 127 | println!(); 128 | dbg!(args); 129 | } 130 | 131 | Ok(()) 132 | } 133 | 134 | fn main() -> ExitCode { 135 | match run() { 136 | Ok(_) => ExitCode::SUCCESS, 137 | Err(err) => { 138 | if matches!(err, Error::Cli(_)) { 139 | eprintln!("{}", Args::HELP); 140 | } 141 | 142 | eprintln!("Error: {err}"); 143 | for source in err.sources().skip(1) { 144 | eprintln!(" Caused by: {source}"); 145 | } 146 | 147 | ExitCode::FAILURE 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/traits.rs: -------------------------------------------------------------------------------- 1 | use crate::CliError; 2 | use std::ffi::OsString; 3 | use std::num::{ParseFloatError, ParseIntError}; 4 | use std::path::PathBuf; 5 | use std::str::FromStr; 6 | 7 | /// An extension trait for `Option` that provides some parsers that are useful for CLIs. 8 | pub trait ArgExt { 9 | /// Parse an argument into a `String`. 10 | /// 11 | /// # Errors 12 | /// 13 | /// Returns `Err` if the argument is `None` or not valid UTF-8. 14 | fn parse_str(self, name: N) -> Result 15 | where 16 | N: Into; 17 | 18 | /// Parse an argument into a `PathBuf`. 19 | /// 20 | /// # Errors 21 | /// 22 | /// Returns `Err` if the argument is `None`. 23 | fn parse_path(self, name: N) -> Result 24 | where 25 | N: Into; 26 | 27 | /// Parse an argument into an `OsString`. 28 | /// 29 | /// # Errors 30 | /// 31 | /// Returns `Err` if the argument is `None`. 32 | fn parse_osstr(self, name: N) -> Result 33 | where 34 | N: Into; 35 | 36 | /// Parse an argument into a primitive integer. 37 | /// 38 | /// # Errors 39 | /// 40 | /// Returns `Err` if the argument is `None` or not a valid integer. 41 | fn parse_int(self, name: N) -> Result 42 | where 43 | N: Into, 44 | T: FromStr; 45 | 46 | /// Parse an argument into a primitive floating point number. 47 | /// 48 | /// # Errors 49 | /// 50 | /// Returns `Err` if the argument is `None` or not valid floating point number. 51 | fn parse_float(self, name: N) -> Result 52 | where 53 | N: Into, 54 | T: FromStr; 55 | } 56 | 57 | /// An extension trait for required arguments. 58 | pub trait RequiredArgExt { 59 | /// The inner type that the trait methods return. 60 | /// 61 | /// For `Option`, this would be `type Inner = T;`. 62 | type Inner; 63 | 64 | /// Unwrap an argument that is required by the CLI. 65 | /// 66 | /// # Errors 67 | /// 68 | /// Returns `Err` if the argument is `None`. 69 | fn required(self, name: N) -> Result 70 | where 71 | N: Into; 72 | } 73 | 74 | impl ArgExt for Option { 75 | fn parse_str(self, name: N) -> Result 76 | where 77 | N: Into, 78 | { 79 | let name = name.into(); 80 | self.ok_or_else(|| CliError::MissingValue(name.clone()))? 81 | .into_string() 82 | .map_err(|err| CliError::ParseStrError(name, err)) 83 | } 84 | 85 | fn parse_path(self, name: N) -> Result 86 | where 87 | N: Into, 88 | { 89 | Ok(self 90 | .ok_or_else(|| CliError::MissingValue(name.into()))? 91 | .into()) 92 | } 93 | 94 | fn parse_osstr(self, name: N) -> Result 95 | where 96 | N: Into, 97 | { 98 | self.ok_or_else(|| CliError::MissingValue(name.into())) 99 | } 100 | 101 | fn parse_int(self, name: N) -> Result 102 | where 103 | N: Into, 104 | T: FromStr, 105 | { 106 | let name = name.into(); 107 | 108 | self.clone().parse_str(&name).and_then(|string| { 109 | string 110 | .parse::() 111 | .map_err(|err| CliError::ParseIntError(name, self.unwrap(), err)) 112 | }) 113 | } 114 | 115 | fn parse_float(self, name: N) -> Result 116 | where 117 | N: Into, 118 | T: FromStr, 119 | { 120 | let name = name.into(); 121 | 122 | self.clone().parse_str(&name).and_then(|string| { 123 | string 124 | .parse::() 125 | .map_err(|err| CliError::ParseFloatError(name, self.unwrap(), err)) 126 | }) 127 | } 128 | } 129 | 130 | impl ArgExt for OsString { 131 | fn parse_str(self, name: N) -> Result 132 | where 133 | N: Into, 134 | { 135 | let name = name.into(); 136 | self.into_string() 137 | .map_err(|err| CliError::ParseStrError(name, err)) 138 | } 139 | 140 | fn parse_path(self, _name: N) -> Result 141 | where 142 | N: Into, 143 | { 144 | Ok(self.into()) 145 | } 146 | 147 | fn parse_osstr(self, _name: N) -> Result 148 | where 149 | N: Into, 150 | { 151 | Ok(self) 152 | } 153 | 154 | fn parse_int(self, name: N) -> Result 155 | where 156 | N: Into, 157 | T: FromStr, 158 | { 159 | let name = name.into(); 160 | 161 | self.clone().parse_str(&name).and_then(|string| { 162 | string 163 | .parse::() 164 | .map_err(|err| CliError::ParseIntError(name, self, err)) 165 | }) 166 | } 167 | 168 | fn parse_float(self, name: N) -> Result 169 | where 170 | N: Into, 171 | T: FromStr, 172 | { 173 | let name = name.into(); 174 | 175 | self.clone().parse_str(&name).and_then(|string| { 176 | string 177 | .parse::() 178 | .map_err(|err| CliError::ParseFloatError(name, self, err)) 179 | }) 180 | } 181 | } 182 | 183 | impl RequiredArgExt for Option { 184 | type Inner = T; 185 | 186 | fn required(self, name: N) -> Result 187 | where 188 | N: Into, 189 | { 190 | self.ok_or_else(|| CliError::MissingRequired(name.into())) 191 | } 192 | } 193 | 194 | impl RequiredArgExt for Vec { 195 | type Inner = Vec; 196 | 197 | fn required(self, name: N) -> Result 198 | where 199 | N: Into, 200 | { 201 | if self.is_empty() { 202 | Err(CliError::MissingRequired(name.into())) 203 | } else { 204 | Ok(self) 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "basic-toml" 7 | version = "0.1.8" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "2db21524cad41c5591204d22d75e1970a2d1f71060214ca931dc7d5afe2c14e5" 10 | dependencies = [ 11 | "serde", 12 | ] 13 | 14 | [[package]] 15 | name = "error-iter" 16 | version = "0.4.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "8070547d90d1b98debb6626421d742c897942bbb78f047694a5eb769495eccd6" 19 | 20 | [[package]] 21 | name = "example-basic" 22 | version = "0.1.0" 23 | dependencies = [ 24 | "error-iter", 25 | "onlyargs", 26 | ] 27 | 28 | [[package]] 29 | name = "example-full" 30 | version = "0.1.0" 31 | dependencies = [ 32 | "error-iter", 33 | "onlyargs", 34 | "onlyerror", 35 | ] 36 | 37 | [[package]] 38 | name = "example-full-derive" 39 | version = "0.1.0" 40 | dependencies = [ 41 | "error-iter", 42 | "onlyargs", 43 | "onlyargs_derive", 44 | "onlyerror", 45 | ] 46 | 47 | [[package]] 48 | name = "glob" 49 | version = "0.3.1" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 52 | 53 | [[package]] 54 | name = "itoa" 55 | version = "1.0.10" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" 58 | 59 | [[package]] 60 | name = "myn" 61 | version = "0.2.1" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "51eb7addae0a5fbc6616160ee79bb7244eb644e2d18becf2f8f03603e06de63a" 64 | 65 | [[package]] 66 | name = "once_cell" 67 | version = "1.19.0" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 70 | 71 | [[package]] 72 | name = "onlyargs" 73 | version = "0.2.0" 74 | dependencies = [ 75 | "error-iter", 76 | ] 77 | 78 | [[package]] 79 | name = "onlyargs_derive" 80 | version = "0.2.0" 81 | dependencies = [ 82 | "myn", 83 | "onlyargs", 84 | "trybuild", 85 | ] 86 | 87 | [[package]] 88 | name = "onlyerror" 89 | version = "0.1.4" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "8c26d4ea2ccd9b7acedc478853805606e60c5b7b7aedfc1c54ed6d1fdc0587db" 92 | dependencies = [ 93 | "myn", 94 | ] 95 | 96 | [[package]] 97 | name = "proc-macro2" 98 | version = "1.0.78" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" 101 | dependencies = [ 102 | "unicode-ident", 103 | ] 104 | 105 | [[package]] 106 | name = "quote" 107 | version = "1.0.35" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 110 | dependencies = [ 111 | "proc-macro2", 112 | ] 113 | 114 | [[package]] 115 | name = "ryu" 116 | version = "1.0.16" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" 119 | 120 | [[package]] 121 | name = "serde" 122 | version = "1.0.196" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" 125 | dependencies = [ 126 | "serde_derive", 127 | ] 128 | 129 | [[package]] 130 | name = "serde_derive" 131 | version = "1.0.196" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" 134 | dependencies = [ 135 | "proc-macro2", 136 | "quote", 137 | "syn", 138 | ] 139 | 140 | [[package]] 141 | name = "serde_json" 142 | version = "1.0.113" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" 145 | dependencies = [ 146 | "itoa", 147 | "ryu", 148 | "serde", 149 | ] 150 | 151 | [[package]] 152 | name = "syn" 153 | version = "2.0.49" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "915aea9e586f80826ee59f8453c1101f9d1c4b3964cd2460185ee8e299ada496" 156 | dependencies = [ 157 | "proc-macro2", 158 | "quote", 159 | "unicode-ident", 160 | ] 161 | 162 | [[package]] 163 | name = "termcolor" 164 | version = "1.4.1" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 167 | dependencies = [ 168 | "winapi-util", 169 | ] 170 | 171 | [[package]] 172 | name = "trybuild" 173 | version = "1.0.89" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "9a9d3ba662913483d6722303f619e75ea10b7855b0f8e0d72799cf8621bb488f" 176 | dependencies = [ 177 | "basic-toml", 178 | "glob", 179 | "once_cell", 180 | "serde", 181 | "serde_derive", 182 | "serde_json", 183 | "termcolor", 184 | ] 185 | 186 | [[package]] 187 | name = "unicode-ident" 188 | version = "1.0.12" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 191 | 192 | [[package]] 193 | name = "winapi" 194 | version = "0.3.9" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 197 | dependencies = [ 198 | "winapi-i686-pc-windows-gnu", 199 | "winapi-x86_64-pc-windows-gnu", 200 | ] 201 | 202 | [[package]] 203 | name = "winapi-i686-pc-windows-gnu" 204 | version = "0.4.0" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 207 | 208 | [[package]] 209 | name = "winapi-util" 210 | version = "0.1.6" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" 213 | dependencies = [ 214 | "winapi", 215 | ] 216 | 217 | [[package]] 218 | name = "winapi-x86_64-pc-windows-gnu" 219 | version = "0.4.0" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 222 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Only argument parsing! Nothing more. 2 | //! 3 | //! `onlyargs` is an obsessively tiny argument parsing library. It provides a basic trait and helper 4 | //! functions for parsing arguments from the environment. 5 | //! 6 | //! Implement the [`OnlyArgs`] trait on your own argument type and use any of the parser functions 7 | //! to create your CLI. The trait can also be derived with the [`onlyargs_derive`] crate if you are 8 | //! OK with an opinionated parser and just want to reduce the amount of boilerplate in your code. 9 | //! 10 | //! [`onlyargs_derive`]: https://docs.rs/onlyargs_derive 11 | 12 | #![forbid(unsafe_code)] 13 | #![deny(clippy::all)] 14 | #![deny(clippy::pedantic)] 15 | 16 | use std::env; 17 | use std::ffi::OsString; 18 | use std::fmt::Display; 19 | 20 | pub mod traits; 21 | 22 | /// Argument parsing errors. 23 | #[derive(Debug)] 24 | pub enum CliError { 25 | /// An argument requires a value, but one was not provided. 26 | MissingValue(String), 27 | 28 | /// A required argument was not provided. 29 | MissingRequired(String), 30 | 31 | /// An argument requires a value, but parsing it as a `bool` failed. 32 | ParseBoolError(String, OsString, std::str::ParseBoolError), 33 | 34 | /// An argument requires a value, but parsing it as a `char` failed. 35 | ParseCharError(String, OsString, std::char::ParseCharError), 36 | 37 | /// An argument requires a value, but parsing it as a floating-point number failed. 38 | ParseFloatError(String, OsString, std::num::ParseFloatError), 39 | 40 | /// An argument requires a value, but parsing it as an integer failed. 41 | ParseIntError(String, OsString, std::num::ParseIntError), 42 | 43 | /// An argument requires a value, but parsing it as a `String` failed. 44 | ParseStrError(String, OsString), 45 | 46 | /// An unknown argument was provided. 47 | Unknown(OsString), 48 | } 49 | 50 | /// The primary argument parser trait. 51 | /// 52 | /// This trait can be derived with the [`onlyargs_derive`](https://docs.rs/onlyargs_derive) crate. 53 | /// 54 | /// See the [`parse`] function for more information. 55 | pub trait OnlyArgs { 56 | /// The application help string. 57 | const HELP: &'static str = concat!( 58 | env!("CARGO_PKG_NAME"), 59 | " v", 60 | env!("CARGO_PKG_VERSION"), 61 | "\n", 62 | env!("CARGO_PKG_DESCRIPTION"), 63 | "\n", 64 | ); 65 | 66 | /// The application name and version. 67 | const VERSION: &'static str = concat!( 68 | env!("CARGO_PKG_NAME"), 69 | " v", 70 | env!("CARGO_PKG_VERSION"), 71 | "\n", 72 | ); 73 | 74 | /// Construct a type that implements this trait. 75 | /// 76 | /// Each argument is provided as an [`OsString`]. 77 | /// 78 | /// # Errors 79 | /// 80 | /// Returns `Err` if the command line arguments cannot be parsed to `Self`. 81 | fn parse(args: Vec) -> Result 82 | where 83 | Self: Sized; 84 | 85 | /// Print the application help string and exit the process. 86 | fn help() -> ! { 87 | eprintln!("{}", Self::HELP); 88 | std::process::exit(0); 89 | } 90 | 91 | /// Print the application name and version and exit the process. 92 | fn version() -> ! { 93 | eprintln!("{}", Self::VERSION); 94 | std::process::exit(0); 95 | } 96 | } 97 | 98 | impl Display for CliError { 99 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 100 | match self { 101 | Self::MissingValue(arg) => write!(f, "Missing value for argument `{arg}`"), 102 | Self::MissingRequired(arg) => write!(f, "Missing required argument `{arg}`"), 103 | Self::ParseBoolError(arg, value, _) => write!( 104 | f, 105 | "Bool parsing error for argument `{arg}`: value={value:?}" 106 | ), 107 | Self::ParseCharError(arg, value, _) => write!( 108 | f, 109 | "Char parsing error for argument `{arg}`: value={value:?}" 110 | ), 111 | Self::ParseFloatError(arg, value, _) => write!( 112 | f, 113 | "Float parsing error for argument `{arg}`: value={value:?}" 114 | ), 115 | Self::ParseIntError(arg, value, _) => { 116 | write!(f, "Int parsing error for argument `{arg}`: value={value:?}") 117 | } 118 | Self::ParseStrError(arg, value) => write!( 119 | f, 120 | "String parsing error for argument `{arg}`: value={value:?}" 121 | ), 122 | Self::Unknown(arg) => write!(f, "Unknown argument: {arg:?}"), 123 | } 124 | } 125 | } 126 | 127 | impl std::error::Error for CliError { 128 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 129 | match self { 130 | Self::ParseBoolError(_, _, err) => Some(err), 131 | Self::ParseCharError(_, _, err) => Some(err), 132 | Self::ParseFloatError(_, _, err) => Some(err), 133 | Self::ParseIntError(_, _, err) => Some(err), 134 | _ => None, 135 | } 136 | } 137 | } 138 | 139 | /// Type constructor for argument parser. 140 | /// 141 | /// Given a type that implements [`OnlyArgs`], this function will construct the type from the 142 | /// current environment. 143 | /// 144 | /// # Errors 145 | /// 146 | /// Returns `Err` if arguments from the environment cannot be parsed to `T`. 147 | /// 148 | /// # Example 149 | /// 150 | /// ``` 151 | /// # use std::ffi::OsString; 152 | /// # use onlyargs::{CliError, OnlyArgs}; 153 | /// struct Args { 154 | /// verbose: bool, 155 | /// } 156 | /// 157 | /// impl OnlyArgs for Args { 158 | /// const HELP: &'static str = onlyargs::impl_help!(); 159 | /// const VERSION: &'static str = onlyargs::impl_version!(); 160 | /// 161 | /// fn parse(args: Vec) -> Result { 162 | /// let mut verbose = false; 163 | /// 164 | /// for arg in args.into_iter() { 165 | /// match arg.to_str() { 166 | /// Some("--help") | Some("-h") => { 167 | /// Self::help(); 168 | /// } 169 | /// Some("--version") | Some("-V") => { 170 | /// Self::version(); 171 | /// } 172 | /// Some("--verbose") | Some("-v") => { 173 | /// verbose = true; 174 | /// } 175 | /// Some("--") => break, 176 | /// _ => return Err(CliError::Unknown(arg)), 177 | /// } 178 | /// } 179 | /// 180 | /// Ok(Self { verbose }) 181 | /// } 182 | /// } 183 | /// 184 | /// let args: Args = onlyargs::parse()?; 185 | /// 186 | /// // Returns a string like "onlyargs v0.1.0" 187 | /// assert_eq!(Args::VERSION, format!("onlyargs v{}\n", env!("CARGO_PKG_VERSION"))); 188 | /// 189 | /// if args.verbose { 190 | /// println!("Verbose output is enabled"); 191 | /// } 192 | /// # Ok::<(), CliError>(()) 193 | /// ``` 194 | pub fn parse() -> Result { 195 | T::parse(env::args_os().skip(1).collect()) 196 | } 197 | 198 | mod macros { 199 | /// Creates a generic `HELP` string for [`OnlyArgs`] implementations. 200 | /// 201 | /// The string will take the following form, filling in details from the package's `Cargo.toml`: 202 | /// 203 | /// ```text 204 | /// {package-name} v{package-version} 205 | /// {package-description} 206 | /// ``` 207 | /// 208 | /// [`OnlyArgs`]: crate::OnlyArgs 209 | #[macro_export] 210 | macro_rules! impl_help { 211 | () => { 212 | concat!( 213 | env!("CARGO_PKG_NAME"), 214 | " v", 215 | env!("CARGO_PKG_VERSION"), 216 | "\n", 217 | env!("CARGO_PKG_DESCRIPTION"), 218 | "\n", 219 | ) 220 | }; 221 | } 222 | 223 | /// Creates a generic `VERSION` string for [`OnlyArgs`] implementations. 224 | /// 225 | /// The string will take the following form, filling in details from the package's `Cargo.toml`: 226 | /// 227 | /// ```text 228 | /// {package-name} v{package-version} 229 | /// ``` 230 | /// 231 | /// [`OnlyArgs`]: crate::OnlyArgs 232 | #[macro_export] 233 | macro_rules! impl_version { 234 | () => { 235 | concat!( 236 | env!("CARGO_PKG_NAME"), 237 | " v", 238 | env!("CARGO_PKG_VERSION"), 239 | "\n", 240 | ); 241 | }; 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /onlyargs_derive/src/parser.rs: -------------------------------------------------------------------------------- 1 | use myn::prelude::*; 2 | use proc_macro::{Delimiter, Ident, Literal, Span, TokenStream}; 3 | 4 | #[derive(Debug)] 5 | pub(crate) struct ArgumentStruct { 6 | pub(crate) name: Ident, 7 | pub(crate) flags: Vec, 8 | pub(crate) options: Vec, 9 | pub(crate) positional: Option, 10 | pub(crate) doc: Vec, 11 | pub(crate) footer: Vec, 12 | } 13 | 14 | #[derive(Debug)] 15 | pub(crate) enum Argument { 16 | Flag(ArgFlag), 17 | Option(ArgOption), 18 | } 19 | 20 | #[derive(Debug)] 21 | pub(crate) struct ArgFlag { 22 | pub(crate) name: Ident, 23 | pub(crate) short: Option, 24 | pub(crate) doc: Vec, 25 | pub(crate) default: bool, 26 | pub(crate) output: bool, 27 | } 28 | 29 | #[derive(Debug)] 30 | pub(crate) struct ArgOption { 31 | pub(crate) name: Ident, 32 | pub(crate) short: Option, 33 | pub(crate) ty_help: ArgType, 34 | pub(crate) doc: Vec, 35 | pub(crate) default: Option, 36 | pub(crate) property: ArgProperty, 37 | } 38 | 39 | #[derive(Copy, Clone, Debug)] 40 | pub(crate) struct ArgView<'a> { 41 | pub(crate) name: &'a Ident, 42 | pub(crate) short: Option, 43 | pub(crate) ty_help: Option, 44 | pub(crate) doc: &'a [String], 45 | } 46 | 47 | #[derive(Copy, Clone, Debug)] 48 | pub(crate) enum ArgType { 49 | Float, 50 | Integer, 51 | OsString, 52 | Path, 53 | String, 54 | } 55 | 56 | #[derive(Copy, Clone, Debug)] 57 | pub(crate) enum ArgProperty { 58 | Required, 59 | Optional, 60 | MultiValue { required: bool }, 61 | Positional { required: bool }, 62 | } 63 | 64 | impl ArgumentStruct { 65 | pub(crate) fn parse(input: TokenStream) -> Result { 66 | let mut input = input.into_token_iter(); 67 | let attrs = input.parse_attributes()?; 68 | input.parse_visibility()?; 69 | input.expect_ident("struct")?; 70 | 71 | let name = input.try_ident()?; 72 | let content = input.expect_group(Delimiter::Brace)?; 73 | let fields = Argument::parse(content)?; 74 | 75 | let mut flags = vec![]; 76 | let mut options = vec![]; 77 | let mut positional = None; 78 | 79 | for field in fields { 80 | match field { 81 | Argument::Flag(flag) => flags.push(flag), 82 | Argument::Option(opt) => match (opt.property, &positional) { 83 | (ArgProperty::Positional { .. }, None) => positional = Some(opt), 84 | (ArgProperty::Positional { .. }, Some(_)) => { 85 | return Err(spanned_error( 86 | "Positional arguments can only be specified once.", 87 | opt.name.span(), 88 | )); 89 | } 90 | _ => options.push(opt), 91 | }, 92 | } 93 | } 94 | 95 | let doc = get_doc_comment(&attrs) 96 | .into_iter() 97 | .map(trim_with_indent) 98 | .collect(); 99 | 100 | let footer = get_attr_strings(&attrs, "footer") 101 | .into_iter() 102 | .map(|line| line.trim_end().to_string()) 103 | .collect(); 104 | 105 | match input.next() { 106 | None => Ok(Self { 107 | name, 108 | flags, 109 | options, 110 | positional, 111 | doc, 112 | footer, 113 | }), 114 | tree => Err(spanned_error("Unexpected token", tree.as_span())), 115 | } 116 | } 117 | } 118 | 119 | impl Argument { 120 | fn parse(mut input: TokenIter) -> Result, TokenStream> { 121 | let mut args = vec![]; 122 | 123 | while input.peek().is_some() { 124 | let attrs = input.parse_attributes()?; 125 | 126 | // Parse attributes 127 | let doc = get_doc_comment(&attrs) 128 | .into_iter() 129 | .map(trim_with_indent) 130 | .collect(); 131 | let mut default = None; 132 | let mut long = false; 133 | let mut short = None; 134 | let mut required = false; 135 | let mut positional = false; 136 | 137 | for mut attr in attrs { 138 | let name = attr.name.to_string(); 139 | match name.as_str() { 140 | "default" => { 141 | let mut stream = attr.tree.expect_group(Delimiter::Parenthesis)?; 142 | 143 | default = Some(stream.try_lit().or_else(|_| { 144 | stream 145 | .try_ident() 146 | .and_then(|ident| match ident.to_string().as_str() { 147 | boolean @ ("true" | "false") => Ok(Literal::string(boolean)), 148 | _ => Err(spanned_error("Unexpected identifier", ident.span())), 149 | }) 150 | })?); 151 | } 152 | "long" => long = true, 153 | "positional" => positional = true, 154 | "required" => required = true, 155 | "short" => { 156 | let mut stream = attr.tree.expect_group(Delimiter::Parenthesis)?; 157 | let lit = stream.try_lit()?; 158 | 159 | short = Some(lit.as_char()?); 160 | } 161 | _ => (), 162 | } 163 | } 164 | 165 | input.parse_visibility()?; 166 | let name = input.try_ident()?; 167 | input.expect_punct(':')?; 168 | let (path, span) = input.parse_path()?; 169 | let _ = input.expect_punct(','); 170 | 171 | let short = if long { 172 | None 173 | } else { 174 | short.or_else(|| { 175 | // TODO: Add an attribute to disable short names 176 | name.to_string().chars().find(char::is_ascii_alphabetic) 177 | }) 178 | }; 179 | 180 | if path == "bool" { 181 | if required { 182 | return Err(spanned_error( 183 | "#[required] can only be used on `Vec`", 184 | span, 185 | )); 186 | } 187 | if positional { 188 | return Err(spanned_error( 189 | "#[positional] can only be used on `Vec`", 190 | span, 191 | )); 192 | } 193 | 194 | let mut flag = ArgFlag::new(name, short, doc); 195 | match default { 196 | Some(lit) if lit.to_string() == r#""true""# => flag.default = true, 197 | _ => (), 198 | } 199 | args.push(Self::Flag(flag)); 200 | } else { 201 | let mut opt = ArgOption::new(span, name, short, doc, &path)?; 202 | 203 | apply_default(span, &mut opt, default)?; 204 | apply_required(span, &mut opt, required)?; 205 | apply_positional(span, &mut opt, positional)?; 206 | 207 | if let Some(default) = opt.default.as_ref() { 208 | let default = default.to_string(); 209 | if let Some(line) = opt.doc.last_mut() { 210 | line.push_str(&format!(" [default: {default}]")); 211 | } else { 212 | opt.doc.push(format!("[default: {default}]")); 213 | } 214 | } else if matches!( 215 | opt.property, 216 | ArgProperty::Required 217 | | ArgProperty::Positional { required: true } 218 | | ArgProperty::MultiValue { required: true } 219 | ) { 220 | if let Some(line) = opt.doc.last_mut() { 221 | line.push_str(" [required]"); 222 | } else { 223 | opt.doc.push("[required]".to_string()); 224 | } 225 | } 226 | 227 | args.push(Self::Option(opt)); 228 | } 229 | } 230 | 231 | Ok(args) 232 | } 233 | } 234 | 235 | fn apply_default( 236 | span: Span, 237 | opt: &mut ArgOption, 238 | default: Option, 239 | ) -> Result<(), TokenStream> { 240 | match (default.is_some(), &opt.property) { 241 | (true, ArgProperty::Required) => opt.default = default, 242 | (true, _) => { 243 | return Err(spanned_error( 244 | "#[default(...)] can only be used on primitive types", 245 | span, 246 | )); 247 | } 248 | (false, _) => (), 249 | } 250 | 251 | Ok(()) 252 | } 253 | 254 | fn apply_required(span: Span, opt: &mut ArgOption, required: bool) -> Result<(), TokenStream> { 255 | match (required, &mut opt.property) { 256 | (false, _) => (), 257 | (true, ArgProperty::MultiValue { required }) => *required = true, 258 | _ => { 259 | return Err(spanned_error( 260 | "#[required] can only be used on `Vec`", 261 | span, 262 | )); 263 | } 264 | } 265 | 266 | Ok(()) 267 | } 268 | 269 | fn apply_positional(span: Span, opt: &mut ArgOption, positional: bool) -> Result<(), TokenStream> { 270 | match (positional, &opt.property) { 271 | (true, ArgProperty::MultiValue { required }) => { 272 | opt.property = ArgProperty::Positional { 273 | required: *required, 274 | } 275 | } 276 | (true, _) => { 277 | return Err(spanned_error( 278 | "#[positional] can only be used on `Vec`", 279 | span, 280 | )); 281 | } 282 | (false, _) => (), 283 | } 284 | 285 | Ok(()) 286 | } 287 | 288 | impl ArgFlag { 289 | fn new(name: Ident, short: Option, doc: Vec) -> Self { 290 | ArgFlag { 291 | name, 292 | short, 293 | doc, 294 | default: false, 295 | output: true, 296 | } 297 | } 298 | 299 | pub(crate) fn new_priv(name: Ident, short: Option, doc: Vec) -> Self { 300 | ArgFlag { 301 | name, 302 | short, 303 | doc, 304 | default: false, 305 | output: false, 306 | } 307 | } 308 | 309 | pub(crate) fn as_view(&self) -> ArgView { 310 | ArgView { 311 | name: &self.name, 312 | short: self.short, 313 | ty_help: None, 314 | doc: &self.doc, 315 | } 316 | } 317 | } 318 | 319 | // We have to check multiple possible paths for types that are not included in 320 | // `std::prelude`. The type system is not available here, so we need to make some educated 321 | // guesses about field types. 322 | const REQUIRED_PATHS: [&str; 4] = [ 323 | "::std::path::PathBuf", 324 | "std::path::PathBuf", 325 | "path::PathBuf", 326 | "PathBuf", 327 | ]; 328 | const REQUIRED_OS_STRINGS: [&str; 4] = [ 329 | "::std::ffi::OsString", 330 | "std::ffi::OsString", 331 | "ffi::OsString", 332 | "OsString", 333 | ]; 334 | const REQUIRED_FLOATS: [&str; 2] = ["f32", "f64"]; 335 | const REQUIRED_INTEGERS: [&str; 12] = [ 336 | "i8", "i16", "i32", "i64", "i128", "isize", "u8", "u16", "u32", "u64", "u128", "usize", 337 | ]; 338 | const MULTI_PATHS: [&str; 4] = [ 339 | "Vec<::std::path::PathBuf>", 340 | "Vec", 341 | "Vec", 342 | "Vec", 343 | ]; 344 | const MULTI_OS_STRINGS: [&str; 4] = [ 345 | "Vec<::std::ffi::OsString>", 346 | "Vec", 347 | "Vec", 348 | "Vec", 349 | ]; 350 | const MULTI_FLOATS: [&str; 2] = ["Vec", "Vec"]; 351 | const MULTI_INTEGERS: [&str; 12] = [ 352 | "Vec", 353 | "Vec", 354 | "Vec", 355 | "Vec", 356 | "Vec", 357 | "Vec", 358 | "Vec", 359 | "Vec", 360 | "Vec", 361 | "Vec", 362 | "Vec", 363 | "Vec", 364 | ]; 365 | const OPTIONAL_PATHS: [&str; 4] = [ 366 | "Option<::std::path::PathBuf>", 367 | "Option", 368 | "Option", 369 | "Option", 370 | ]; 371 | const OPTIONAL_OS_STRINGS: [&str; 4] = [ 372 | "Option<::std::ffi::OsString>", 373 | "Option", 374 | "Option", 375 | "Option", 376 | ]; 377 | const OPTIONAL_FLOATS: [&str; 2] = ["Option", "Option"]; 378 | const OPTIONAL_INTEGERS: [&str; 12] = [ 379 | "Option", 380 | "Option", 381 | "Option", 382 | "Option", 383 | "Option", 384 | "Option", 385 | "Option", 386 | "Option", 387 | "Option", 388 | "Option", 389 | "Option", 390 | "Option", 391 | ]; 392 | 393 | impl ArgOption { 394 | fn new( 395 | span: Span, 396 | name: Ident, 397 | short: Option, 398 | doc: Vec, 399 | path: &str, 400 | ) -> Result { 401 | // Parse the argument type and decide what properties it should start with. 402 | let property = if OPTIONAL_PATHS.contains(&path) 403 | || OPTIONAL_OS_STRINGS.contains(&path) 404 | || OPTIONAL_FLOATS.contains(&path) 405 | || OPTIONAL_INTEGERS.contains(&path) 406 | || path == "Option" 407 | { 408 | ArgProperty::Optional 409 | } else if MULTI_PATHS.contains(&path) 410 | || MULTI_OS_STRINGS.contains(&path) 411 | || MULTI_FLOATS.contains(&path) 412 | || MULTI_INTEGERS.contains(&path) 413 | || path == "Vec" 414 | { 415 | ArgProperty::MultiValue { required: false } 416 | } else if REQUIRED_PATHS.contains(&path) 417 | || REQUIRED_OS_STRINGS.contains(&path) 418 | || REQUIRED_FLOATS.contains(&path) 419 | || REQUIRED_INTEGERS.contains(&path) 420 | || path == "String" 421 | { 422 | ArgProperty::Required 423 | } else { 424 | return Err(spanned_error( 425 | "Expected bool, PathBuf, String, OsString, integer, or float", 426 | span, 427 | )); 428 | }; 429 | 430 | // Decide the type to show in the help message. 431 | let ty_help = if OPTIONAL_PATHS.contains(&path) 432 | || REQUIRED_PATHS.contains(&path) 433 | || MULTI_PATHS.contains(&path) 434 | { 435 | ArgType::Path 436 | } else if OPTIONAL_OS_STRINGS.contains(&path) 437 | || REQUIRED_OS_STRINGS.contains(&path) 438 | || MULTI_OS_STRINGS.contains(&path) 439 | { 440 | ArgType::OsString 441 | } else if path == "String" || path == "Vec" || path == "Option" { 442 | ArgType::String 443 | } else if OPTIONAL_FLOATS.contains(&path) 444 | || REQUIRED_FLOATS.contains(&path) 445 | || MULTI_FLOATS.contains(&path) 446 | { 447 | ArgType::Float 448 | } else if OPTIONAL_INTEGERS.contains(&path) 449 | || REQUIRED_INTEGERS.contains(&path) 450 | || MULTI_INTEGERS.contains(&path) 451 | { 452 | ArgType::Integer 453 | } else { 454 | unreachable!(); 455 | }; 456 | 457 | Ok(ArgOption { 458 | name, 459 | short, 460 | ty_help, 461 | doc, 462 | default: None, 463 | property, 464 | }) 465 | } 466 | 467 | pub(crate) fn as_view(&self) -> ArgView { 468 | ArgView { 469 | name: &self.name, 470 | short: self.short, 471 | ty_help: Some(self.ty_help), 472 | doc: &self.doc, 473 | } 474 | } 475 | } 476 | 477 | impl ArgType { 478 | pub(crate) fn as_str(&self) -> &str { 479 | match self { 480 | Self::Float => " FLOAT", 481 | Self::Integer => " INTEGER", 482 | Self::OsString | Self::String => " STRING", 483 | Self::Path => " PATH", 484 | } 485 | } 486 | 487 | pub(crate) fn converter(&self) -> &str { 488 | match self { 489 | Self::Float | Self::Integer => "", 490 | Self::OsString | Self::Path | Self::String => ".into()", 491 | } 492 | } 493 | } 494 | 495 | #[allow(clippy::needless_pass_by_value)] 496 | fn trim_with_indent(line: String) -> String { 497 | line.strip_prefix(' ') 498 | .unwrap_or(&line) 499 | .trim_end() 500 | .to_string() 501 | } 502 | -------------------------------------------------------------------------------- /onlyargs_derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Derive macro for [`onlyargs`](https://docs.rs/onlyargs). 2 | //! 3 | //! The parser generated by this macro is very opinionated. The implementation attempts to be as 4 | //! light as possible while also being usable for most applications. 5 | //! 6 | //! # Example 7 | //! 8 | //! ``` 9 | //! use onlyargs_derive::OnlyArgs; 10 | //! 11 | //! /// Doc comments will appear in your application's help text. 12 | //! /// 13 | //! /// Features: 14 | //! /// - Supports multi-line strings. 15 | //! /// - Supports indentation. 16 | //! #[derive(Debug, OnlyArgs)] 17 | //! #[footer = "Footer attributes will be included at the bottom of the help message."] 18 | //! #[footer = ""] 19 | //! #[footer = "Features:"] 20 | //! #[footer = " - Also supports multi-line strings."] 21 | //! #[footer = " - Also supports indentation."] 22 | //! struct Args { 23 | //! /// Optional output path. 24 | //! output: Option, 25 | //! 26 | //! /// Enable verbose output. 27 | //! verbose: bool, 28 | //! } 29 | //! 30 | //! let args: Args = onlyargs::parse()?; 31 | //! 32 | //! if let Some(output) = args.output { 33 | //! if args.verbose { 34 | //! eprintln!("Creating file: `{path}`", path = output.display()); 35 | //! } 36 | //! 37 | //! // Do something with `output`... 38 | //! } 39 | //! # Ok::<_, onlyargs::CliError>(()) 40 | //! ``` 41 | //! 42 | //! # DSL reference 43 | //! 44 | //! Only structs with named fields are supported. Doc comments are used for the generated help text. 45 | //! Argument names are generated automatically from field names with only a few rules: 46 | //! 47 | //! - Long argument names start with `--`, ASCII alphabetic characters are made lowercase, and all 48 | //! `_` characters are replaced with `-`. 49 | //! - Short argument names use the first ASCII alphabetic character of the field name following a 50 | //! `-`. Short arguments are not allowed to be duplicated. 51 | //! - This behavior can be suppressed with the `#[long]` attribute (see below). 52 | //! - Alternatively, the `#[short('…')]` attribute can be used to set a specific short name. 53 | //! 54 | //! # Footer 55 | //! 56 | //! The `#[footer = "..."]` attribute on the argument struct will add lines to the bottom of the 57 | //! help message. It can be used multiple times. 58 | //! 59 | //! # Provided arguments 60 | //! 61 | //! `--help|-h` and `--version|-V` arguments are automatically generated. When the parser encounters 62 | //! either, it will print the help or version message and exit the application with exit code 0. 63 | //! 64 | //! # Field attributes 65 | //! 66 | //! Parsing options are configurable with the following attributes: 67 | //! 68 | //! - `#[long]`: Only generate long argument names like `--help`. Short args like `-h` are generated 69 | //! by default, and this attribute suppresses that behavior. 70 | //! - `#[short('N')]`: Generate a short argument name with the given character. In this example, it 71 | //! will be `-N`. 72 | //! - If `#[long]` and `#[short]` are used together, `#[long]` takes precedence. 73 | //! - `#[default(T)]`: Specify a default value for an argument. Where `T` is a literal value. 74 | //! - Accepts string literals for `PathBuf`. 75 | //! - Accepts numeric literals for numeric types. 76 | //! - Accepts `true` and `false` idents and `"true"` and `"false"` string literals for `boolean`. 77 | //! - `#[required]`: Can be used on `Vec` to require at least one value. This ensures the vector 78 | //! is never empty. 79 | //! - `#[positional]`: Makes a `Vec` the dumping ground for positional arguments. 80 | //! 81 | //! # Supported types 82 | //! 83 | //! Here is the list of supported field "primitive" types: 84 | //! 85 | //! | Type | Description | 86 | //! |------------------|--------------------------------------------------| 87 | //! | `bool` | Defines a flag. | 88 | //! | `f32`\|`f64` | Floating point number option. | 89 | //! | `i8`\|`u8` | 8-bit integer option. | 90 | //! | `i16`\|`u16` | 16-bit integer option. | 91 | //! | `i32`\|`u32` | 32-bit integer option. | 92 | //! | `i64`\|`u64` | 64-bit integer option. | 93 | //! | `i128`\|`u128` | 128-bit integer option. | 94 | //! | `isize`\|`usize` | Pointer-sized integer option. | 95 | //! | `OsString` | A string option with platform-specific encoding. | 96 | //! | `PathBuf` | A file system path option. | 97 | //! | `String` | UTF-8 encoded string option. | 98 | //! 99 | //! Additionally, some wrapper and composite types are also available, where the type `T` must be 100 | //! one of the primitive types listed above (except `bool`). 101 | //! 102 | //! | Type | Description | 103 | //! |-------------|------------------------------------------------------------| 104 | //! | `Option` | An optional argument. | 105 | //! | `Vec` | Multivalue and positional arguments (see `#[positional]`). | 106 | //! 107 | //! In argument parsing parlance, "flags" are simple boolean values; the argument does not require 108 | //! a value. For example, the argument `--help`. 109 | //! 110 | //! "Options" carry a value and the argument parser requires the value to directly follow the 111 | //! argument name. Arguments can be made optional with `Option`. 112 | //! 113 | //! Multivalue arguments can be passed on the command line by using the same argument multiple 114 | //! times. 115 | 116 | #![forbid(unsafe_code)] 117 | #![deny(clippy::all)] 118 | #![deny(clippy::pedantic)] 119 | #![allow(clippy::let_underscore_untyped)] 120 | 121 | use crate::parser::{ArgFlag, ArgOption, ArgProperty, ArgType, ArgView, ArgumentStruct}; 122 | use myn::utils::spanned_error; 123 | use proc_macro::{Ident, Span, TokenStream}; 124 | use std::{collections::HashMap, fmt::Write as _, str::FromStr as _}; 125 | 126 | mod parser; 127 | 128 | /// See the [root module documentation](crate) for the DSL specification. 129 | #[allow(clippy::too_many_lines)] 130 | #[proc_macro_derive( 131 | OnlyArgs, 132 | attributes(footer, default, long, positional, required, short) 133 | )] 134 | pub fn derive_parser(input: TokenStream) -> TokenStream { 135 | let ast = match ArgumentStruct::parse(input) { 136 | Ok(ast) => ast, 137 | Err(err) => return err, 138 | }; 139 | 140 | let mut flags = vec![ 141 | ArgFlag::new_priv( 142 | Ident::new("help", Span::call_site()), 143 | Some('h'), 144 | vec!["Show this help message.".to_string()], 145 | ), 146 | ArgFlag::new_priv( 147 | Ident::new("version", Span::call_site()), 148 | Some('V'), 149 | vec!["Show the application version.".to_string()], 150 | ), 151 | ]; 152 | flags.extend(ast.flags); 153 | 154 | // De-dupe short args. 155 | let mut dupes = HashMap::new(); 156 | for flag in &flags { 157 | if let Err(err) = dedupe(&mut dupes, flag.as_view()) { 158 | return err; 159 | } 160 | } 161 | for opt in &ast.options { 162 | if let Err(err) = dedupe(&mut dupes, opt.as_view()) { 163 | return err; 164 | } 165 | } 166 | 167 | // Produce help text for all arguments. 168 | let max_width = get_max_width(flags.iter().map(ArgFlag::as_view)); 169 | let flags_help = flags 170 | .iter() 171 | .map(|arg| to_help(arg.as_view(), max_width)) 172 | .collect::(); 173 | 174 | let max_width = get_max_width(ast.options.iter().map(ArgOption::as_view)); 175 | let options_help = ast 176 | .options 177 | .iter() 178 | .map(|arg| to_help(arg.as_view(), max_width)) 179 | .collect::(); 180 | 181 | let positional_header = ast 182 | .positional 183 | .as_ref() 184 | .map(|opt| format!(" [{}...]", opt.name)) 185 | .unwrap_or_default(); 186 | let positional_help = ast 187 | .positional 188 | .as_ref() 189 | .map(|opt| format!("\n{}:\n {}\n", opt.name, opt.doc.join("\n "))) 190 | .unwrap_or_default(); 191 | 192 | // Produce variables for argument parser state. 193 | let flags_vars = 194 | flags 195 | .iter() 196 | .filter(|&flag| flag.output) 197 | .fold(String::new(), |mut flags, flag| { 198 | write!( 199 | flags, 200 | "let mut {name} = {default:?};", 201 | name = flag.name, 202 | default = flag.default, 203 | ) 204 | .unwrap(); 205 | flags 206 | }); 207 | let options_vars = ast 208 | .options 209 | .iter() 210 | .map(|opt| { 211 | let name = &opt.name; 212 | if let Some(default) = opt.default.as_ref() { 213 | format!("let mut {name} = {default}{};", opt.ty_help.converter()) 214 | } else { 215 | match opt.property { 216 | ArgProperty::Optional | ArgProperty::Required => { 217 | format!("let mut {name} = None;") 218 | } 219 | ArgProperty::MultiValue { .. } => { 220 | format!("let mut {name} = vec![];") 221 | } 222 | ArgProperty::Positional { .. } => unreachable!(), 223 | } 224 | } 225 | }) 226 | .collect::(); 227 | let positional_var = ast 228 | .positional 229 | .as_ref() 230 | .map(|opt| { 231 | let name = &opt.name; 232 | format!("let mut {name} = vec![];") 233 | }) 234 | .unwrap_or_default(); 235 | 236 | // Produce matchers for parser. 237 | let flags_matchers = 238 | flags 239 | .iter() 240 | .filter(|&flag| flag.output) 241 | .fold(String::new(), |mut matchers, flag| { 242 | let name = &flag.name; 243 | let short = flag 244 | .short 245 | .map(|ch| format!(r#"| Some("-{ch}")"#)) 246 | .unwrap_or_default(); 247 | 248 | write!( 249 | matchers, 250 | r#"Some("--{arg}") {short} => {name} = true,"#, 251 | arg = to_arg_name(name) 252 | ) 253 | .unwrap(); 254 | matchers 255 | }); 256 | let options_matchers = ast.options.iter().fold(String::new(), |mut matchers, opt| { 257 | let name = &opt.name; 258 | let short = opt 259 | .short 260 | .map(|ch| format!(r#"| Some(arg_name_ @ "-{ch}")"#)) 261 | .unwrap_or_default(); 262 | let assignment = if opt.default.is_some() { 263 | match opt.ty_help { 264 | ArgType::Float => format!("{name} = args.next().parse_float(arg_name_)?"), 265 | ArgType::Integer => format!("{name} = args.next().parse_int(arg_name_)?"), 266 | ArgType::OsString => format!("{name} = args.next().parse_osstr(arg_name_)?"), 267 | ArgType::Path => format!("{name} = args.next().parse_path(arg_name_)?"), 268 | ArgType::String => format!("{name} = args.next().parse_str(arg_name_)?"), 269 | } 270 | } else { 271 | match opt.property { 272 | ArgProperty::Optional | ArgProperty::Required => match opt.ty_help { 273 | ArgType::Float => format!("{name} = Some(args.next().parse_float(arg_name_)?)"), 274 | ArgType::Integer => format!("{name} = Some(args.next().parse_int(arg_name_)?)"), 275 | ArgType::OsString => { 276 | format!("{name} = Some(args.next().parse_osstr(arg_name_)?)") 277 | } 278 | ArgType::Path => format!("{name} = Some(args.next().parse_path(arg_name_)?)"), 279 | ArgType::String => format!("{name} = Some(args.next().parse_str(arg_name_)?)"), 280 | }, 281 | ArgProperty::MultiValue { .. } => match opt.ty_help { 282 | ArgType::Float => format!("{name}.push(args.next().parse_float(arg_name_)?)"), 283 | ArgType::Integer => format!("{name}.push(args.next().parse_int(arg_name_)?)"), 284 | ArgType::OsString => { 285 | format!("{name}.push(args.next().parse_osstr(arg_name_)?)") 286 | } 287 | ArgType::Path => format!("{name}.push(args.next().parse_path(arg_name_)?)"), 288 | ArgType::String => format!("{name}.push(args.next().parse_str(arg_name_)?)"), 289 | }, 290 | ArgProperty::Positional { .. } => unreachable!(), 291 | } 292 | }; 293 | 294 | write!( 295 | matchers, 296 | r#"Some(arg_name_ @ "--{arg}") {short} => {assignment},"#, 297 | arg = to_arg_name(name) 298 | ) 299 | .unwrap(); 300 | matchers 301 | }); 302 | let positional_matcher = match ast.positional.as_ref() { 303 | Some(opt) => { 304 | let name = &opt.name; 305 | let value = match opt.ty_help { 306 | ArgType::Float => r#"arg.parse_float("")?"#, 307 | ArgType::Integer => r#"arg.parse_int("")?"#, 308 | ArgType::OsString => r#"arg.parse_osstr("")?"#, 309 | ArgType::Path => r#"arg.parse_path("")?"#, 310 | ArgType::String => r#"arg.parse_str("")?"#, 311 | }; 312 | 313 | format!( 314 | r#" 315 | Some("--") => {{ 316 | for arg in args {{ 317 | {name}.push({value}); 318 | }} 319 | break; 320 | }} 321 | _ => {name}.push({value}), 322 | "# 323 | ) 324 | } 325 | None => r#" 326 | Some("--") => break, 327 | _ => return Err(::onlyargs::CliError::Unknown(arg)), 328 | "# 329 | .to_string(), 330 | }; 331 | 332 | // Produce identifiers for args constructor. 333 | let flags_idents = flags 334 | .iter() 335 | .filter_map(|flag| flag.output.then_some(format!("{},", flag.name))) 336 | .collect::(); 337 | let options_idents = ast 338 | .options 339 | .iter() 340 | .map(|opt| { 341 | let name = &opt.name; 342 | let optional = matches!( 343 | opt.property, 344 | ArgProperty::Optional 345 | | ArgProperty::Positional { required: false } 346 | | ArgProperty::MultiValue { required: false } 347 | ); 348 | if opt.default.is_some() || optional { 349 | format!("{name},") 350 | } else { 351 | format!( 352 | r#"{name}: {name}.required("--{arg}")?,"#, 353 | arg = to_arg_name(name) 354 | ) 355 | } 356 | }) 357 | .collect::(); 358 | let positional_ident = ast 359 | .positional 360 | .map(|opt| { 361 | if matches!(opt.property, ArgProperty::Positional { required: true }) { 362 | format!( 363 | r#"{}: {}.required("{arg}")?,"#, 364 | opt.name, 365 | opt.name, 366 | arg = to_arg_name(&opt.name), 367 | ) 368 | } else { 369 | format!("{},", opt.name) 370 | } 371 | }) 372 | .unwrap_or_default(); 373 | 374 | let name = ast.name; 375 | let doc_comment = if ast.doc.is_empty() { 376 | String::new() 377 | } else { 378 | format!("\n{}\n", ast.doc.join("\n")) 379 | }; 380 | let footer = if ast.footer.is_empty() { 381 | String::new() 382 | } else { 383 | format!("\n{}\n", ast.footer.join("\n")) 384 | }; 385 | let bin_name = std::env::var_os("CARGO_BIN_NAME").and_then(|name| name.into_string().ok()); 386 | let help_impl = if bin_name.is_none() { 387 | r#"fn help() -> ! { 388 | let bin_name = ::std::env::args_os() 389 | .next() 390 | .unwrap_or_default() 391 | .to_string_lossy() 392 | .into_owned(); 393 | ::std::eprintln!("{}", Self::HELP.replace("{bin_name}", &bin_name)); 394 | ::std::process::exit(0); 395 | }"# 396 | } else { 397 | "" 398 | }; 399 | let bin_name = bin_name.unwrap_or_else(|| "{bin_name}".to_string()); 400 | 401 | // Produce final code. 402 | let code = TokenStream::from_str(&format!( 403 | r#" 404 | impl ::onlyargs::OnlyArgs for {name} {{ 405 | const HELP: &'static str = ::std::concat!( 406 | env!("CARGO_PKG_NAME"), 407 | " v", 408 | env!("CARGO_PKG_VERSION"), 409 | "\n", 410 | env!("CARGO_PKG_DESCRIPTION"), 411 | "\n", 412 | {doc_comment:?}, 413 | "\nUsage:\n ", 414 | {bin_name:?}, 415 | " [flags] [options]", 416 | {positional_header:?}, 417 | "\n\nFlags:\n", 418 | {flags_help:?}, 419 | "\nOptions:\n", 420 | {options_help:?}, 421 | {positional_help:?}, 422 | {footer:?}, 423 | ); 424 | 425 | const VERSION: &'static str = concat!( 426 | env!("CARGO_PKG_NAME"), 427 | " v", 428 | env!("CARGO_PKG_VERSION"), 429 | "\n", 430 | ); 431 | 432 | {help_impl} 433 | 434 | fn parse(args: Vec<::std::ffi::OsString>) -> 435 | ::std::result::Result 436 | {{ 437 | use ::onlyargs::traits::*; 438 | use ::std::option::Option::{{None, Some}}; 439 | use ::std::result::Result::{{Err, Ok}}; 440 | 441 | {flags_vars} 442 | {options_vars} 443 | {positional_var} 444 | 445 | let mut args = args.into_iter(); 446 | while let Some(arg) = args.next() {{ 447 | match arg.to_str() {{ 448 | // TODO: Add an attribute to disable help/version. 449 | Some("--help") | Some("-h") => Self::help(), 450 | Some("--version") | Some("-V") => Self::version(), 451 | {flags_matchers} 452 | {options_matchers} 453 | {positional_matcher} 454 | }} 455 | }} 456 | 457 | Ok(Self {{ 458 | {flags_idents} 459 | {options_idents} 460 | {positional_ident} 461 | }}) 462 | }} 463 | }} 464 | "# 465 | )); 466 | 467 | match code { 468 | Ok(stream) => stream, 469 | Err(err) => spanned_error(err.to_string(), Span::call_site()), 470 | } 471 | } 472 | 473 | // 1 hyphen + 1 char + 1 trailing space. 474 | const SHORT_PAD: usize = 3; 475 | // 2 leading spaces + 2 hyphens + 2 trailing spaces. 476 | const LONG_PAD: usize = 6; 477 | 478 | fn to_arg_name(ident: &Ident) -> String { 479 | let mut name = ident.to_string().replace('_', "-"); 480 | name.make_ascii_lowercase(); 481 | 482 | name 483 | } 484 | 485 | fn to_help(view: ArgView, max_width: usize) -> String { 486 | let name = to_arg_name(view.name); 487 | let ty = match view.ty_help.as_ref() { 488 | Some(ty_help) => ty_help.as_str(), 489 | None => "", 490 | }; 491 | let pad = " ".repeat(max_width + LONG_PAD); 492 | let help = view.doc.join(&format!("\n{pad}")); 493 | 494 | let width = max_width - name.len(); 495 | if let Some(ch) = view.short { 496 | let width = width - SHORT_PAD; 497 | 498 | format!(" -{ch} --{name}{ty:(iter: I) -> usize 505 | where 506 | I: Iterator>, 507 | { 508 | iter.fold(0, |acc, view| { 509 | let short = view.short.map(|_| SHORT_PAD).unwrap_or_default(); 510 | let ty = match view.ty_help.as_ref() { 511 | Some(ty_help) => ty_help.as_str(), 512 | None => "", 513 | }; 514 | 515 | acc.max(view.name.to_string().len() + ty.len() + short) 516 | }) 517 | } 518 | 519 | fn dedupe<'a>(dupes: &mut HashMap, arg: ArgView<'a>) -> Result<(), TokenStream> { 520 | if let Some(ch) = arg.short { 521 | if let Some(other) = dupes.get(&ch) { 522 | let msg = 523 | format!("Only one short arg is allowed. `-{ch}` also used on field `{other}`"); 524 | 525 | return Err(spanned_error(msg, arg.name.span())); 526 | } 527 | 528 | dupes.insert(ch, arg.name); 529 | } 530 | 531 | Ok(()) 532 | } 533 | --------------------------------------------------------------------------------