├── derive ├── LICENSE-MIT ├── LICENSE-APACHE ├── src │ ├── shared.rs │ ├── lib.rs │ ├── derive_parser.rs │ ├── error.rs │ ├── derive_value_enum.rs │ ├── derive_subcommand.rs │ └── common.rs └── Cargo.toml ├── rustfmt.toml ├── .gitignore ├── tests ├── ui │ ├── parser-manual-impl.rs │ ├── args-flatten-subcommand.stderr │ ├── args-flatten-unnamed.stderr │ ├── args-flatten-unnamed.rs │ ├── no-generic.rs │ ├── args-duplicated-names.rs │ ├── args-invalid-ignore_case.stderr │ ├── args-flatten-subcommand.rs │ ├── args-unsupported-data.rs │ ├── args-subcommand-non-subcommand.rs │ ├── parser-unsupported-data.rs │ ├── args-type-unparsable.rs │ ├── value_enum-unsupported-data.rs │ ├── args-invalid-names.rs │ ├── args-invalid-ignore_case.rs │ ├── args-flatten-non-args.rs │ ├── subcommand-unsupported-data.rs │ ├── args-duplicated-names.stderr │ ├── parser-manual-impl.stderr │ ├── no-generic.stderr │ ├── args-unsupported-data.stderr │ ├── value_enum-unsupported-data.stderr │ ├── parser-unsupported-data.stderr │ ├── subcommand-unsupported-data.stderr │ ├── args-invalid-names.stderr │ ├── args-subcommand-non-subcommand.stderr │ ├── args-type-unparsable.stderr │ └── args-flatten-non-args.stderr ├── ui.rs ├── smoke.rs ├── value_enum.rs ├── help.rs └── parse.rs ├── test-suite ├── src │ └── bin │ │ ├── simple-clap.rs │ │ ├── criterion-clap.rs │ │ ├── deno-clap.rs │ │ ├── simple-none.rs │ │ ├── common │ │ ├── simple.rs │ │ └── criterion.rs │ │ ├── simple-palc.rs │ │ ├── simple-argh.rs │ │ ├── criterion-palc.rs │ │ ├── deno-palc.rs │ │ └── criterion-argh.rs └── Cargo.toml ├── LICENSE-MIT ├── bench.txt ├── Cargo.toml ├── .github └── workflows │ └── ci.yaml ├── bench.sh ├── src ├── util.rs ├── shared.rs ├── help.rs ├── values.rs ├── refl.rs ├── lib.rs └── error.rs ├── README.md ├── LICENSE-APACHE └── Cargo.lock /derive/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /derive/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /derive/src/shared.rs: -------------------------------------------------------------------------------- 1 | ../../src/shared.rs -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | use_small_heuristics = "Max" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | result 3 | result-* 4 | 5 | perf*.data* 6 | flamegraph*.svg 7 | -------------------------------------------------------------------------------- /tests/ui/parser-manual-impl.rs: -------------------------------------------------------------------------------- 1 | use palc::Parser; 2 | 3 | struct Cli {} 4 | 5 | impl Parser for Cli {} 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /tests/ui.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | #[ignore = "unstable across compiler versions"] 3 | fn ui() { 4 | let t = trybuild::TestCases::new(); 5 | t.compile_fail("tests/ui/*.rs"); 6 | } 7 | -------------------------------------------------------------------------------- /test-suite/src/bin/simple-clap.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | #[path = "./common/simple.rs"] 4 | mod cli; 5 | 6 | fn main() { 7 | let cli = cli::Cli::parse(); 8 | std::hint::black_box(&cli); 9 | } 10 | -------------------------------------------------------------------------------- /test-suite/src/bin/criterion-clap.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, ValueEnum}; 2 | 3 | #[path = "./common/criterion.rs"] 4 | mod cli; 5 | 6 | fn main() { 7 | let cli = cli::Cli::parse(); 8 | std::hint::black_box(&cli); 9 | } 10 | -------------------------------------------------------------------------------- /test-suite/src/bin/deno-clap.rs: -------------------------------------------------------------------------------- 1 | use clap::{Args, Parser, Subcommand, ValueEnum}; 2 | 3 | #[path = "./common/deno.rs"] 4 | mod cli; 5 | 6 | fn main() { 7 | let cli = cli::Opt::parse(); 8 | std::hint::black_box(&cli); 9 | } 10 | -------------------------------------------------------------------------------- /tests/ui/args-flatten-subcommand.stderr: -------------------------------------------------------------------------------- 1 | error[E0080]: evaluation panicked: cannot flatten an Args with subcommand 2 | --> tests/ui/args-flatten-subcommand.rs:6:12 3 | | 4 | 6 | inner: Inner, 5 | | ^^^^^ evaluation of `_` failed here 6 | -------------------------------------------------------------------------------- /tests/ui/args-flatten-unnamed.stderr: -------------------------------------------------------------------------------- 1 | error[E0080]: evaluation panicked: TODO: cannot arg(flatten) positional arguments yet 2 | --> tests/ui/args-flatten-unnamed.rs:6:12 3 | | 4 | 6 | inner: Inner, 5 | | ^^^^^ evaluation of `_` failed here 6 | -------------------------------------------------------------------------------- /tests/ui/args-flatten-unnamed.rs: -------------------------------------------------------------------------------- 1 | use palc::Args; 2 | 3 | #[derive(Args)] 4 | struct Outer { 5 | #[command(flatten)] 6 | inner: Inner, 7 | } 8 | 9 | #[derive(Args)] 10 | struct Inner { 11 | unnamed: String, 12 | } 13 | 14 | fn main() {} 15 | -------------------------------------------------------------------------------- /tests/ui/no-generic.rs: -------------------------------------------------------------------------------- 1 | #[derive(palc::Parser)] 2 | pub struct Parser { 3 | a: T, 4 | } 5 | 6 | #[derive(palc::Args)] 7 | pub struct Args { 8 | a: T, 9 | } 10 | 11 | #[derive(palc::Subcommand)] 12 | pub enum Subcommand { 13 | A(T), 14 | } 15 | 16 | fn main() {} 17 | -------------------------------------------------------------------------------- /tests/ui/args-duplicated-names.rs: -------------------------------------------------------------------------------- 1 | #[derive(palc::Args)] 2 | struct Short { 3 | #[arg(short)] 4 | hello: i32, 5 | #[arg(short)] 6 | hell: i32, 7 | 8 | #[arg(long)] 9 | long1: i32, 10 | #[arg(long = "long1")] 11 | long2: i32, 12 | } 13 | 14 | fn main() {} 15 | -------------------------------------------------------------------------------- /tests/ui/args-invalid-ignore_case.stderr: -------------------------------------------------------------------------------- 1 | error[E0080]: evaluation panicked: `arg(ignore_case)` only supports `ValueEnum` that contains no UPPERCASE variants 2 | --> tests/ui/args-invalid-ignore_case.rs:8:12 3 | | 4 | 8 | upper: UpperCase, 5 | | ^^^^^^^^^ evaluation of `_` failed here 6 | -------------------------------------------------------------------------------- /test-suite/src/bin/simple-none.rs: -------------------------------------------------------------------------------- 1 | //! This example is a baseline for size comparison, 2 | //! to count for `std::env::args_os`'s cost. 3 | use std::hint::black_box; 4 | 5 | fn main() { 6 | let args = std::env::args_os().collect::>(); 7 | println!("hello world"); 8 | black_box(args); 9 | } 10 | -------------------------------------------------------------------------------- /tests/ui/args-flatten-subcommand.rs: -------------------------------------------------------------------------------- 1 | use palc::{Args, Subcommand}; 2 | 3 | #[derive(Args)] 4 | struct Outer { 5 | #[command(flatten)] 6 | inner: Inner, 7 | } 8 | 9 | #[derive(Args)] 10 | struct Inner { 11 | #[command(subcommand)] 12 | subcmd: Subcmd, 13 | } 14 | 15 | #[derive(Subcommand)] 16 | enum Subcmd {} 17 | 18 | fn main() {} 19 | -------------------------------------------------------------------------------- /test-suite/src/bin/common/simple.rs: -------------------------------------------------------------------------------- 1 | //! This is an example for a dead simple program. 2 | use std::path::PathBuf; 3 | 4 | use super::Parser; 5 | 6 | #[derive(Parser)] 7 | /// My great app. 8 | pub struct Cli { 9 | /// Print more text. 10 | #[arg(long, short)] 11 | verbose: bool, 12 | /// The file to process. 13 | file: PathBuf, 14 | } 15 | -------------------------------------------------------------------------------- /tests/ui/args-unsupported-data.rs: -------------------------------------------------------------------------------- 1 | use palc::{__private::Args, Args}; 2 | 3 | #[derive(Args)] 4 | struct Unit; 5 | 6 | #[derive(Args)] 7 | struct Tuple(); 8 | 9 | #[derive(Args)] 10 | union Union { 11 | a: (), 12 | } 13 | 14 | #[derive(Args)] 15 | enum Enum {} 16 | 17 | struct AssertImplArgs 18 | where 19 | Unit: Args, 20 | Tuple: Args, 21 | Enum: Args; 22 | 23 | fn main() {} 24 | -------------------------------------------------------------------------------- /tests/ui/args-subcommand-non-subcommand.rs: -------------------------------------------------------------------------------- 1 | //! TODO: Suboptimal, see args-flatten-non-args for blockage. 2 | 3 | #[derive(palc::Args)] 4 | struct Cli1 { 5 | #[command(subcommand)] 6 | deep: Deep, 7 | } 8 | 9 | struct Deep {} 10 | 11 | #[derive(palc::Args)] 12 | struct Cli2 { 13 | #[command(subcommand)] 14 | cmd: Subargs, 15 | } 16 | 17 | #[derive(palc::Args)] 18 | struct Subargs {} 19 | 20 | fn main() {} 21 | -------------------------------------------------------------------------------- /tests/ui/parser-unsupported-data.rs: -------------------------------------------------------------------------------- 1 | use palc::Parser; 2 | 3 | #[derive(Parser)] 4 | struct Unit; 5 | 6 | #[derive(Parser)] 7 | struct Tuple(); 8 | 9 | #[derive(Parser)] 10 | union Union { 11 | a: (), 12 | } 13 | 14 | // TODO: top-level subcommands. 15 | #[derive(Parser)] 16 | enum Enum {} 17 | 18 | struct AssertImplParser 19 | where 20 | Unit: Parser, 21 | Tuple: Parser, 22 | Enum: Parser; 23 | 24 | fn main() {} 25 | -------------------------------------------------------------------------------- /tests/ui/args-type-unparsable.rs: -------------------------------------------------------------------------------- 1 | #[derive(palc::Args)] 2 | struct Cli { 3 | positional: MyType, 4 | #[arg(long)] 5 | arg: MyType, 6 | 7 | vec: Vec, 8 | #[arg(long)] 9 | vec_arg: Vec, 10 | } 11 | 12 | #[derive(palc::Args)] 13 | struct Cli2 { 14 | option_vec: Option>, 15 | #[arg(long)] 16 | option_vec_arg: Option>, 17 | } 18 | 19 | struct MyType; 20 | 21 | fn main() {} 22 | -------------------------------------------------------------------------------- /tests/ui/value_enum-unsupported-data.rs: -------------------------------------------------------------------------------- 1 | use palc::{__private::ValueEnum, ValueEnum}; 2 | 3 | #[derive(ValueEnum)] 4 | struct Unit; 5 | 6 | #[derive(ValueEnum)] 7 | struct Tuple(); 8 | 9 | #[derive(ValueEnum)] 10 | struct Named {} 11 | 12 | #[derive(ValueEnum)] 13 | union Union { 14 | a: (), 15 | } 16 | 17 | struct AssertImpl 18 | where 19 | Unit: ValueEnum, 20 | Tuple: ValueEnum, 21 | Named: ValueEnum; 22 | 23 | fn main() {} 24 | -------------------------------------------------------------------------------- /tests/ui/args-invalid-names.rs: -------------------------------------------------------------------------------- 1 | #[derive(palc::Args)] 2 | struct Cli { 3 | #[arg(long = "")] 4 | long_empty: i32, 5 | #[arg(long = "\0")] 6 | long_control: i32, 7 | #[arg(long = "-foo")] 8 | long_dash: i32, 9 | #[arg(long = "a=b")] 10 | long_eq: i32, 11 | 12 | #[arg(short = '-')] 13 | short_dash: i32, 14 | #[arg(short = '坏')] 15 | short_unicode: i32, 16 | #[arg(short)] 17 | 首字符Unicode: i32, 18 | } 19 | 20 | fn main() {} 21 | -------------------------------------------------------------------------------- /tests/ui/args-invalid-ignore_case.rs: -------------------------------------------------------------------------------- 1 | use palc::{Args, ValueEnum}; 2 | 3 | #[derive(Args)] 4 | struct Cli1 { 5 | #[arg(long, value_enum, ignore_case = true)] 6 | lower: LowerCase, 7 | #[arg(long, value_enum, ignore_case = true)] 8 | upper: UpperCase, 9 | } 10 | 11 | #[derive(ValueEnum)] 12 | enum LowerCase { 13 | Hello, 14 | } 15 | 16 | #[derive(ValueEnum)] 17 | #[value(rename_all = "UPPER")] 18 | enum UpperCase { 19 | Hello, 20 | } 21 | 22 | fn main() {} 23 | -------------------------------------------------------------------------------- /test-suite/src/bin/simple-palc.rs: -------------------------------------------------------------------------------- 1 | use palc::Parser; 2 | 3 | #[expect(dead_code, reason = "fields are unused and just for testing")] 4 | #[path = "./common/simple.rs"] 5 | mod cli; 6 | 7 | fn main() { 8 | let cli = cli::Cli::parse(); 9 | std::hint::black_box(&cli); 10 | } 11 | 12 | #[cfg(feature = "full-featured")] 13 | #[test] 14 | fn help() { 15 | let help = cli::Cli::render_long_help("me"); 16 | println!("{help}"); 17 | assert!(help.contains("Usage: me [OPTIONS] ")); 18 | } 19 | -------------------------------------------------------------------------------- /tests/ui/args-flatten-non-args.rs: -------------------------------------------------------------------------------- 1 | //! TODO: This triggers are many duplicated errors. Could be improved, but blocked on rustc. 2 | //! WAIT: 3 | 4 | #[derive(palc::Args)] 5 | struct Cli1 { 6 | #[command(flatten)] 7 | deep: Deep, 8 | } 9 | 10 | struct Deep {} 11 | 12 | #[derive(palc::Args)] 13 | struct Cli2 { 14 | #[command(flatten)] 15 | deep: Subcmd, 16 | } 17 | 18 | #[derive(palc::Subcommand)] 19 | enum Subcmd {} 20 | 21 | fn main() {} 22 | -------------------------------------------------------------------------------- /tests/ui/subcommand-unsupported-data.rs: -------------------------------------------------------------------------------- 1 | use palc::{__private::Subcommand, Subcommand}; 2 | 3 | #[derive(Subcommand)] 4 | struct Unit; 5 | 6 | #[derive(Subcommand)] 7 | struct Tuple(); 8 | 9 | #[derive(Subcommand)] 10 | struct Named { 11 | a: (), 12 | } 13 | 14 | #[derive(Subcommand)] 15 | union Union { 16 | a: (), 17 | } 18 | 19 | struct AssertImplSubcommand 20 | where 21 | Unit: Subcommand, 22 | Tuple: Subcommand, 23 | Named: Subcommand, 24 | Enum: Subcommand; 25 | 26 | fn main() {} 27 | -------------------------------------------------------------------------------- /test-suite/src/bin/simple-argh.rs: -------------------------------------------------------------------------------- 1 | //! See `./common/simple.rs`. 2 | use std::path::PathBuf; 3 | 4 | use argh::FromArgs; 5 | 6 | #[expect(dead_code, reason = "fields are only for testing")] 7 | #[derive(FromArgs)] 8 | /// My great app. 9 | pub struct Cli { 10 | /// print more text 11 | #[argh(switch, short = 'v')] 12 | verbose: bool, 13 | /// the file to process 14 | #[argh(positional)] 15 | file: PathBuf, 16 | } 17 | 18 | fn main() { 19 | let cli: Cli = argh::from_env(); 20 | std::hint::black_box(&cli); 21 | } 22 | -------------------------------------------------------------------------------- /tests/ui/args-duplicated-names.stderr: -------------------------------------------------------------------------------- 1 | error: duplicated argument names 2 | --> tests/ui/args-duplicated-names.rs:6:5 3 | | 4 | 6 | hell: i32, 5 | | ^^^^ 6 | 7 | error: previously defined here 8 | --> tests/ui/args-duplicated-names.rs:4:5 9 | | 10 | 4 | hello: i32, 11 | | ^^^^^ 12 | 13 | error: duplicated argument names 14 | --> tests/ui/args-duplicated-names.rs:10:18 15 | | 16 | 10 | #[arg(long = "long1")] 17 | | ^^^^^^^ 18 | 19 | error: previously defined here 20 | --> tests/ui/args-duplicated-names.rs:9:5 21 | | 22 | 9 | long1: i32, 23 | | ^^^^^ 24 | -------------------------------------------------------------------------------- /tests/ui/parser-manual-impl.stderr: -------------------------------------------------------------------------------- 1 | error[E0277]: the trait bound `Cli: ParserInternal` is not satisfied 2 | --> tests/ui/parser-manual-impl.rs:5:17 3 | | 4 | 5 | impl Parser for Cli {} 5 | | ^^^ unsatisfied trait bound 6 | | 7 | help: the trait `ParserInternal` is not implemented for `Cli` 8 | --> tests/ui/parser-manual-impl.rs:3:1 9 | | 10 | 3 | struct Cli {} 11 | | ^^^^^^^^^^ 12 | note: required by a bound in `Parser` 13 | --> src/lib.rs 14 | | 15 | | pub trait Parser: ParserInternal + Sized + 'static { 16 | | ^^^^^^^^^^^^^^ required by this bound in `Parser` 17 | -------------------------------------------------------------------------------- /test-suite/src/bin/criterion-palc.rs: -------------------------------------------------------------------------------- 1 | use palc::{Parser, ValueEnum}; 2 | 3 | #[expect(dead_code, reason = "fields are unused and just for testing")] 4 | #[path = "./common/criterion.rs"] 5 | mod cli; 6 | 7 | fn main() { 8 | let cli = cli::Cli::parse(); 9 | std::hint::black_box(&cli); 10 | } 11 | 12 | #[cfg(feature = "full-featured")] 13 | #[test] 14 | fn help() { 15 | let help = cli::Cli::render_long_help("me"); 16 | println!("{help}"); 17 | 18 | assert!(help.contains("Usage: me [OPTIONS] [FILTER]...")); 19 | assert!(help.contains("-c, --color ")); 20 | assert!(help.contains("Configure coloring of output.")); 21 | 22 | assert!(help.contains("This executable is a Criterion.rs benchmark.")); 23 | } 24 | -------------------------------------------------------------------------------- /tests/ui/no-generic.stderr: -------------------------------------------------------------------------------- 1 | error: generics are not supported 2 | --> tests/ui/no-generic.rs:1:10 3 | | 4 | 1 | #[derive(palc::Parser)] 5 | | ^^^^^^^^^^^^ 6 | | 7 | = note: this error originates in the derive macro `palc::Parser` (in Nightly builds, run with -Z macro-backtrace for more info) 8 | 9 | error: generics are not supported 10 | --> tests/ui/no-generic.rs:6:10 11 | | 12 | 6 | #[derive(palc::Args)] 13 | | ^^^^^^^^^^ 14 | | 15 | = note: this error originates in the derive macro `palc::Args` (in Nightly builds, run with -Z macro-backtrace for more info) 16 | 17 | error: generics are not supported 18 | --> tests/ui/no-generic.rs:11:10 19 | | 20 | 11 | #[derive(palc::Subcommand)] 21 | | ^^^^^^^^^^^^^^^^ 22 | | 23 | = note: this error originates in the derive macro `palc::Subcommand` (in Nightly builds, run with -Z macro-backtrace for more info) 24 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /test-suite/src/bin/deno-palc.rs: -------------------------------------------------------------------------------- 1 | use palc::{Args, Parser, Subcommand, ValueEnum}; 2 | 3 | #[path = "./common/deno.rs"] 4 | mod cli; 5 | 6 | fn main() { 7 | let cli = cli::Opt::parse(); 8 | std::hint::black_box(&cli); 9 | } 10 | 11 | #[cfg(feature = "full-featured")] 12 | #[test] 13 | fn help() { 14 | let help = cli::Opt::render_long_help("me"); 15 | println!("{help}"); 16 | 17 | assert!(help.contains("A secure JavaScript and TypeScript runtime")); 18 | assert!(help.contains("Usage: me [OPTIONS] [COMMAND")); 19 | assert!(help.contains("-L, --log-level ")); 20 | 21 | // TODO: assert!(help.contains("ENVIRONMENT VARIABLES:")); 22 | } 23 | 24 | #[cfg(feature = "full-featured")] 25 | #[test] 26 | fn help_subcommand() { 27 | let help = cli::Opt::try_parse_from(["me", "compile", "--help"]).err().unwrap().to_string(); 28 | println!("{help}"); 29 | 30 | assert!(help.contains("Compiles the given script into a self contained executable.")); 31 | assert!(help.contains("Usage: me compile [OPTIONS]")); 32 | // assert!(help.contains("-L, --log-level ")); 33 | } 34 | -------------------------------------------------------------------------------- /derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "palc-derive" 3 | version = "0.0.2" 4 | description = "Derive macros for palc. Use the re-exports from palc intead." 5 | readme = false 6 | edition.workspace = true 7 | rust-version.workspace = true 8 | license.workspace = true 9 | repository.workspace = true 10 | categories.workspace = true 11 | keywords.workspace = true 12 | 13 | [lib] 14 | proc-macro = true 15 | 16 | [dependencies] 17 | heck = "0.5.0" 18 | proc-macro2 = "1.0.94" 19 | quote = "1.0.40" 20 | syn = { version = "2.0.100", features = ["derive"] } 21 | 22 | [dev-dependencies] 23 | prettyplease = "0.2.32" 24 | 25 | [lints.clippy] 26 | pedantic = { level = "warn", priority = -1 } 27 | 28 | dbg-macro = "warn" 29 | print_stderr = "warn" 30 | print_stdout = "warn" 31 | todo = "warn" 32 | 33 | # Match sometimes gives more consistency and symmetry. 34 | single_match_else = "allow" 35 | # Configuration structs contain many bools because of interface complexity. 36 | struct_excessive_bools = "allow" 37 | # Parsing and codegen does not really benefit from spliting out. 38 | too_many_lines = "allow" 39 | # index as u8 40 | cast_possible_truncation = "allow" 41 | -------------------------------------------------------------------------------- /tests/ui/args-unsupported-data.stderr: -------------------------------------------------------------------------------- 1 | error: only structs with named fields are supported 2 | --> tests/ui/args-unsupported-data.rs:3:10 3 | | 4 | 3 | #[derive(Args)] 5 | | ^^^^ 6 | | 7 | = note: this error originates in the derive macro `Args` (in Nightly builds, run with -Z macro-backtrace for more info) 8 | 9 | error: only structs with named fields are supported 10 | --> tests/ui/args-unsupported-data.rs:6:10 11 | | 12 | 6 | #[derive(Args)] 13 | | ^^^^ 14 | | 15 | = note: this error originates in the derive macro `Args` (in Nightly builds, run with -Z macro-backtrace for more info) 16 | 17 | error: only structs with named fields are supported 18 | --> tests/ui/args-unsupported-data.rs:9:10 19 | | 20 | 9 | #[derive(Args)] 21 | | ^^^^ 22 | | 23 | = note: this error originates in the derive macro `Args` (in Nightly builds, run with -Z macro-backtrace for more info) 24 | 25 | error: only structs with named fields are supported 26 | --> tests/ui/args-unsupported-data.rs:14:10 27 | | 28 | 14 | #[derive(Args)] 29 | | ^^^^ 30 | | 31 | = note: this error originates in the derive macro `Args` (in Nightly builds, run with -Z macro-backtrace for more info) 32 | -------------------------------------------------------------------------------- /tests/ui/value_enum-unsupported-data.stderr: -------------------------------------------------------------------------------- 1 | error: only enums are supported 2 | --> tests/ui/value_enum-unsupported-data.rs:3:10 3 | | 4 | 3 | #[derive(ValueEnum)] 5 | | ^^^^^^^^^ 6 | | 7 | = note: this error originates in the derive macro `ValueEnum` (in Nightly builds, run with -Z macro-backtrace for more info) 8 | 9 | error: only enums are supported 10 | --> tests/ui/value_enum-unsupported-data.rs:6:10 11 | | 12 | 6 | #[derive(ValueEnum)] 13 | | ^^^^^^^^^ 14 | | 15 | = note: this error originates in the derive macro `ValueEnum` (in Nightly builds, run with -Z macro-backtrace for more info) 16 | 17 | error: only enums are supported 18 | --> tests/ui/value_enum-unsupported-data.rs:9:10 19 | | 20 | 9 | #[derive(ValueEnum)] 21 | | ^^^^^^^^^ 22 | | 23 | = note: this error originates in the derive macro `ValueEnum` (in Nightly builds, run with -Z macro-backtrace for more info) 24 | 25 | error: only enums are supported 26 | --> tests/ui/value_enum-unsupported-data.rs:12:10 27 | | 28 | 12 | #[derive(ValueEnum)] 29 | | ^^^^^^^^^ 30 | | 31 | = note: this error originates in the derive macro `ValueEnum` (in Nightly builds, run with -Z macro-backtrace for more info) 32 | -------------------------------------------------------------------------------- /bench.txt: -------------------------------------------------------------------------------- 1 | # Runs on AMD Ryzen 7 5700G, x86_64-linux. Your result may vary. 2 | # Commit: 329c8c30f6a173d857a4008ac49eb239f7de2c00 3 | # Rust: rustc 1.91.1 4 | 5 | # Note 1: "full-build" and "incremental" columns are using "dev" profile. 6 | # Note 2: Keep in mind that argh supports much fewer features than palc and clap. 7 | # Their numbers cannot be compared directly. 8 | # Note 3: "default" column for "*-clap" have "suggestions" and "color" disabled, 9 | # because palc does not support them yet. 10 | 11 | name minimal default full-build incremental 12 | simple-clap 311KiB 461KiB 0:02.92 0:00.03 13 | simple-argh 38.6KiB 40.9KiB 0:03.26 0:00.03 14 | simple-palc 26.3KiB 33.2KiB 0:02.45 0:00.03 15 | simple-none 9.12KiB 9.12KiB 0:00.12 0:00.03 16 | criterion-clap 440KiB 592KiB 0:03.09 0:00.03 17 | criterion-argh 52.5KiB 57.5KiB 0:03.29 0:00.03 18 | criterion-palc 46.2KiB 57.5KiB 0:02.49 0:00.03 19 | deno-clap 535KiB 681KiB 0:03.18 0:00.03 20 | deno-palc 82.2KiB 115KiB 0:02.64 0:00.03 21 | -------------------------------------------------------------------------------- /tests/ui/parser-unsupported-data.stderr: -------------------------------------------------------------------------------- 1 | error: only structs with named fields are supported yet 2 | --> tests/ui/parser-unsupported-data.rs:3:10 3 | | 4 | 3 | #[derive(Parser)] 5 | | ^^^^^^ 6 | | 7 | = note: this error originates in the derive macro `Parser` (in Nightly builds, run with -Z macro-backtrace for more info) 8 | 9 | error: only structs with named fields are supported yet 10 | --> tests/ui/parser-unsupported-data.rs:6:10 11 | | 12 | 6 | #[derive(Parser)] 13 | | ^^^^^^ 14 | | 15 | = note: this error originates in the derive macro `Parser` (in Nightly builds, run with -Z macro-backtrace for more info) 16 | 17 | error: only structs with named fields are supported yet 18 | --> tests/ui/parser-unsupported-data.rs:9:10 19 | | 20 | 9 | #[derive(Parser)] 21 | | ^^^^^^ 22 | | 23 | = note: this error originates in the derive macro `Parser` (in Nightly builds, run with -Z macro-backtrace for more info) 24 | 25 | error: only structs with named fields are supported yet 26 | --> tests/ui/parser-unsupported-data.rs:15:10 27 | | 28 | 15 | #[derive(Parser)] 29 | | ^^^^^^ 30 | | 31 | = note: this error originates in the derive macro `Parser` (in Nightly builds, run with -Z macro-backtrace for more info) 32 | -------------------------------------------------------------------------------- /derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This is the proc-macro crate of `palc`. 2 | //! 3 | //! See [documentations of `palc`](https://docs.rs/palc). 4 | #![forbid(unsafe_code)] 5 | use proc_macro::TokenStream; 6 | use syn::DeriveInput; 7 | 8 | #[macro_use] 9 | mod error; 10 | 11 | mod common; 12 | mod derive_args; 13 | mod derive_parser; 14 | mod derive_subcommand; 15 | mod derive_value_enum; 16 | 17 | #[allow(unused, reason = "some functions are only used at runtime")] 18 | mod shared; 19 | 20 | #[proc_macro_derive(Parser, attributes(arg, command))] 21 | pub fn derive_parser(tts: TokenStream) -> TokenStream { 22 | let input = syn::parse_macro_input!(tts as DeriveInput); 23 | derive_parser::expand(&input).into() 24 | } 25 | 26 | #[proc_macro_derive(Args, attributes(arg, command))] 27 | pub fn derive_args(tts: TokenStream) -> TokenStream { 28 | let input = syn::parse_macro_input!(tts as DeriveInput); 29 | derive_args::expand(&input).into() 30 | } 31 | 32 | #[proc_macro_derive(Subcommand, attributes(arg, command))] 33 | pub fn derive_subcommand(tts: TokenStream) -> TokenStream { 34 | let input = syn::parse_macro_input!(tts as DeriveInput); 35 | derive_subcommand::expand(&input).into() 36 | } 37 | 38 | #[proc_macro_derive(ValueEnum, attributes(value))] 39 | pub fn derive_value_enum(tts: TokenStream) -> TokenStream { 40 | let input = syn::parse_macro_input!(tts as DeriveInput); 41 | derive_value_enum::expand(&input).into() 42 | } 43 | -------------------------------------------------------------------------------- /tests/ui/subcommand-unsupported-data.stderr: -------------------------------------------------------------------------------- 1 | error: structs are only supported by `derive(Args)`, not by `derive(Subcommand)` 2 | --> tests/ui/subcommand-unsupported-data.rs:3:10 3 | | 4 | 3 | #[derive(Subcommand)] 5 | | ^^^^^^^^^^ 6 | | 7 | = note: this error originates in the derive macro `Subcommand` (in Nightly builds, run with -Z macro-backtrace for more info) 8 | 9 | error: structs are only supported by `derive(Args)`, not by `derive(Subcommand)` 10 | --> tests/ui/subcommand-unsupported-data.rs:6:10 11 | | 12 | 6 | #[derive(Subcommand)] 13 | | ^^^^^^^^^^ 14 | | 15 | = note: this error originates in the derive macro `Subcommand` (in Nightly builds, run with -Z macro-backtrace for more info) 16 | 17 | error: structs are only supported by `derive(Args)`, not by `derive(Subcommand)` 18 | --> tests/ui/subcommand-unsupported-data.rs:9:10 19 | | 20 | 9 | #[derive(Subcommand)] 21 | | ^^^^^^^^^^ 22 | | 23 | = note: this error originates in the derive macro `Subcommand` (in Nightly builds, run with -Z macro-backtrace for more info) 24 | 25 | error: only enums are supported 26 | --> tests/ui/subcommand-unsupported-data.rs:14:10 27 | | 28 | 14 | #[derive(Subcommand)] 29 | | ^^^^^^^^^^ 30 | | 31 | = note: this error originates in the derive macro `Subcommand` (in Nightly builds, run with -Z macro-backtrace for more info) 32 | 33 | error[E0412]: cannot find type `Enum` in this scope 34 | --> tests/ui/subcommand-unsupported-data.rs:24:5 35 | | 36 | 24 | Enum: Subcommand; 37 | | ^^^^ not found in this scope 38 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "3" 3 | members = [".", "derive", "test-suite"] 4 | 5 | [workspace.package] 6 | repository = "https://github.com/oxalica/palc" 7 | license = "MIT OR Apache-2.0" 8 | categories = ["command-line-interface"] 9 | keywords = ["argument", "cli", "arg", "parser", "clap"] 10 | edition = "2024" 11 | # NB. Sync with CI. 12 | rust-version = "1.88" # let-chains 13 | 14 | [package] 15 | name = "palc" 16 | version = "0.0.2" 17 | description = "WIP: Command Line Argument Parser with several opposite design goal from Clap" 18 | exclude = ["*.sh"] # Shortcut scripts. 19 | edition.workspace = true 20 | rust-version.workspace = true 21 | license.workspace = true 22 | repository.workspace = true 23 | categories.workspace = true 24 | keywords.workspace = true 25 | 26 | [features] 27 | default = ["help"] 28 | help = [] 29 | 30 | [dependencies] 31 | os_str_bytes = { version = "7.1.0", default-features = false, features = ["raw_os_str"] } 32 | palc-derive = { path = "./derive", version = "=0.0.2" } 33 | ref-cast = "1.0.24" 34 | 35 | [dev-dependencies] 36 | argh = { version = "0.1.13", default-features = false } 37 | clap = { version = "4.5.35", default-features = false, features = ["std", "derive", "error-context", "help", "usage"] } 38 | expect-test = "1.5.1" 39 | trybuild = { version = "1.0.108", features = ["diff"] } 40 | 41 | [lints.clippy] 42 | dbg-macro = "warn" 43 | # TODO: todo = "warn" 44 | 45 | [profile.minimal] 46 | inherits = "release" 47 | strip = true 48 | opt-level = "z" 49 | lto = true 50 | codegen-units = 1 51 | panic = "abort" 52 | 53 | [package.metadata.docs.rs] 54 | all-features = true 55 | -------------------------------------------------------------------------------- /test-suite/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-suite" 3 | version = "0.1.0" 4 | edition = "2024" 5 | publish = false # Only for testing and benchmarking. 6 | 7 | [features] 8 | clap = ["dep:clap"] 9 | palc = ["dep:palc"] 10 | argh = ["dep:argh"] 11 | 12 | full-featured = [ 13 | "palc?/default", 14 | 15 | # TODO: clap/default also includes `suggestions` and `color`, which are not supported by palc yet. 16 | "clap?/error-context", 17 | "clap?/help", 18 | "clap?/std", 19 | "clap?/usage", 20 | 21 | "argh?/default" 22 | ] 23 | 24 | [dependencies.palc] 25 | path = ".." 26 | default-features = false 27 | optional = true 28 | 29 | [dependencies.clap] 30 | version = "4" 31 | default-features = false 32 | features = ["std", "derive"] 33 | optional = true 34 | 35 | [dependencies.argh] 36 | version = "0.1" 37 | default-features = false 38 | optional = true 39 | 40 | [[bin]] 41 | name = "criterion-palc" 42 | required-features = ["palc"] 43 | 44 | [[bin]] 45 | name = "criterion-clap" 46 | required-features = ["clap"] 47 | 48 | [[bin]] 49 | name = "criterion-argh" 50 | required-features = ["argh"] 51 | 52 | [[bin]] 53 | name = "deno-palc" 54 | required-features = ["palc"] 55 | 56 | [[bin]] 57 | name = "deno-clap" 58 | required-features = ["clap"] 59 | 60 | # There is no "deno-argh", because argh simply does not support that complex use case. 61 | 62 | [[bin]] 63 | name = "simple-palc" 64 | required-features = ["palc"] 65 | 66 | [[bin]] 67 | name = "simple-clap" 68 | required-features = ["clap"] 69 | 70 | [[bin]] 71 | name = "simple-argh" 72 | required-features = ["argh"] 73 | 74 | [[bin]] 75 | name = "simple-none" 76 | -------------------------------------------------------------------------------- /tests/ui/args-invalid-names.stderr: -------------------------------------------------------------------------------- 1 | error: arg(long) name must NOT be empty 2 | --> tests/ui/args-invalid-names.rs:3:18 3 | | 4 | 3 | #[arg(long = "")] 5 | | ^^ 6 | 7 | error: arg(long) name must NOT contain '=' or ASCII control characters 8 | --> tests/ui/args-invalid-names.rs:5:18 9 | | 10 | 5 | #[arg(long = "\0")] 11 | | ^^^^ 12 | 13 | error: arg(long) name is automatically prefixed by "--", and you should not add more "-" prefix 14 | --> tests/ui/args-invalid-names.rs:7:18 15 | | 16 | 7 | #[arg(long = "-foo")] 17 | | ^^^^^^ 18 | 19 | error: arg(long) name must NOT contain '=' or ASCII control characters 20 | --> tests/ui/args-invalid-names.rs:9:18 21 | | 22 | 9 | #[arg(long = "a=b")] 23 | | ^^^^^ 24 | 25 | error: arg(short) name must NOT be '-' or ASCII control characters 26 | --> tests/ui/args-invalid-names.rs:12:19 27 | | 28 | 12 | #[arg(short = '-')] 29 | | ^^^ 30 | 31 | error: Non-ASCII arg(short) name is reserved. Use `arg(long)` instead. A unicode codepoint is not necessarity a "character" in human sense, thus automatic splitting or argument de-bundling may give unexpected results. If you do want this to be supported, convince us by opening an issue. 32 | --> tests/ui/args-invalid-names.rs:14:19 33 | | 34 | 14 | #[arg(short = '坏')] 35 | | ^^^^ 36 | 37 | error: Non-ASCII arg(short) name is reserved. Use `arg(long)` instead. A unicode codepoint is not necessarity a "character" in human sense, thus automatic splitting or argument de-bundling may give unexpected results. If you do want this to be supported, convince us by opening an issue. 38 | --> tests/ui/args-invalid-names.rs:17:5 39 | | 40 | 17 | 首字符Unicode: i32, 41 | | ^^^^^^^^^^^^^ 42 | -------------------------------------------------------------------------------- /derive/src/derive_parser.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Ident, Span, TokenStream}; 2 | use quote::quote; 3 | use syn::{Data, DeriveInput, FieldsNamed}; 4 | 5 | use crate::{ 6 | common::wrap_anon_item, 7 | error::{Result, catch_errors}, 8 | }; 9 | 10 | pub fn expand(input: &DeriveInput) -> TokenStream { 11 | assert_no_generics!(input); 12 | 13 | match catch_errors(|| match &input.data { 14 | Data::Struct(syn::DataStruct { fields: syn::Fields::Named(fields), .. }) => { 15 | try_expand_for_named_struct(input, fields) 16 | } 17 | _ => abort!(Span::call_site(), "only structs with named fields are supported yet"), 18 | }) { 19 | Ok(tts) => wrap_anon_item(tts), 20 | Err(mut tts) => { 21 | tts.extend(wrap_anon_item(fallback(&input.ident))); 22 | tts 23 | } 24 | } 25 | } 26 | 27 | fn fallback(ident: &Ident) -> TokenStream { 28 | quote! { 29 | #[automatically_derived] 30 | impl __rt::Parser for #ident {} 31 | 32 | #[automatically_derived] 33 | impl __rt::ParserInternal for #ident { 34 | fn __parse_toplevel(_: &mut __rt::RawParser, _: &__rt::OsStr) -> __rt::Result { 35 | __rt::unreachable!() 36 | } 37 | } 38 | } 39 | } 40 | 41 | fn try_expand_for_named_struct(input: &DeriveInput, fields: &FieldsNamed) -> Result { 42 | let mut tts = crate::derive_args::try_expand_for_named_struct(input, fields)?; 43 | let ident = &input.ident; 44 | tts.extend(quote! { 45 | #[automatically_derived] 46 | impl __rt::Parser for #ident {} 47 | 48 | #[automatically_derived] 49 | impl __rt::ParserInternal for #ident { 50 | fn __parse_toplevel(__p: &mut __rt::RawParser, __program_name: &__rt::OsStr) -> __rt::Result { 51 | __rt::try_parse_state::<::__State>(__p, __program_name, &mut ()) 52 | } 53 | } 54 | }); 55 | Ok(tts) 56 | } 57 | -------------------------------------------------------------------------------- /tests/smoke.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use palc::{Args, Parser, Subcommand, ValueEnum}; 4 | 5 | /// My great App. 6 | #[derive(Debug, PartialEq, Parser)] 7 | struct MyCli { 8 | name: Option, 9 | 10 | #[command(flatten)] 11 | config: Config, 12 | 13 | #[arg(short = 'v', global = true)] 14 | debug: bool, 15 | 16 | #[arg(long, require_equals = true)] 17 | color: Color, 18 | 19 | #[command(subcommand)] 20 | command: Option, 21 | } 22 | 23 | /// Configure something. 24 | #[derive(Debug, PartialEq, Args)] 25 | struct Config { 26 | #[arg(long)] 27 | config_file: Option, 28 | #[arg(long)] 29 | config: Option, 30 | } 31 | 32 | #[derive(Debug, PartialEq, Subcommand)] 33 | enum Commands { 34 | /// Do something complex 35 | Test { 36 | #[arg(short, long)] 37 | list: bool, 38 | 39 | files: Option>, 40 | }, 41 | Transparent(Config), 42 | /// Do something simple 43 | Unit, 44 | } 45 | 46 | #[derive(Debug, PartialEq, ValueEnum)] 47 | enum Color { 48 | Auto, 49 | Never, 50 | Always, 51 | } 52 | 53 | #[test] 54 | fn smoke() { 55 | let args = MyCli::try_parse_from([ 56 | "foo", 57 | "--color=always", 58 | "--config", 59 | "foo", 60 | "bar", 61 | "test", 62 | "-l", 63 | "-v", 64 | "hello", 65 | "world", 66 | ]) 67 | .unwrap(); 68 | assert_eq!( 69 | args, 70 | MyCli { 71 | color: Color::Always, 72 | name: Some("bar".into()), 73 | config: Config { config_file: None, config: Some("foo".into()) }, 74 | debug: true, 75 | command: Some(Commands::Test { 76 | list: true, 77 | files: Some(vec![PathBuf::from("hello"), PathBuf::from("world")]), 78 | }), 79 | } 80 | ); 81 | } 82 | 83 | #[cfg(feature = "help")] 84 | #[test] 85 | fn help() { 86 | let help = MyCli::render_long_help("me"); 87 | println!("{help}"); 88 | 89 | assert!(help.contains("My great App.")); 90 | assert!(help.contains("Usage: me --color= [OPTIONS]")); 91 | } 92 | -------------------------------------------------------------------------------- /tests/ui/args-subcommand-non-subcommand.stderr: -------------------------------------------------------------------------------- 1 | error[E0277]: `Deep` is not a `palc::Subcommand` 2 | --> tests/ui/args-subcommand-non-subcommand.rs:6:11 3 | | 4 | 6 | deep: Deep, 5 | | ^^^^ this type is expected to have `derive(palc::Subcommand)` but it is not 6 | | 7 | help: the trait `Subcommand` is not implemented for `Deep` 8 | --> tests/ui/args-subcommand-non-subcommand.rs:9:1 9 | | 10 | 9 | struct Deep {} 11 | | ^^^^^^^^^^^ 12 | note: required by a bound in `GetSubcommand::Subcommand` 13 | --> src/runtime.rs 14 | | 15 | | type Subcommand: Subcommand; 16 | | ^^^^^^^^^^ required by this bound in `GetSubcommand::Subcommand` 17 | 18 | error[E0277]: `Subargs` is not a `palc::Subcommand` 19 | --> tests/ui/args-subcommand-non-subcommand.rs:14:10 20 | | 21 | 14 | cmd: Subargs, 22 | | ^^^^^^^ this type is expected to have `derive(palc::Subcommand)` but it is not 23 | | 24 | help: the trait `Subcommand` is not implemented for `Subargs` 25 | --> tests/ui/args-subcommand-non-subcommand.rs:18:1 26 | | 27 | 18 | struct Subargs {} 28 | | ^^^^^^^^^^^^^^ 29 | note: required by a bound in `GetSubcommand::Subcommand` 30 | --> src/runtime.rs 31 | | 32 | | type Subcommand: Subcommand; 33 | | ^^^^^^^^^^ required by this bound in `GetSubcommand::Subcommand` 34 | 35 | error[E0277]: `Deep` is not a `palc::Subcommand` 36 | --> tests/ui/args-subcommand-non-subcommand.rs:6:11 37 | | 38 | 6 | deep: Deep, 39 | | ^^^^ this type is expected to have `derive(palc::Subcommand)` but it is not 40 | | 41 | help: the trait `Subcommand` is not implemented for `Deep` 42 | --> tests/ui/args-subcommand-non-subcommand.rs:9:1 43 | | 44 | 9 | struct Deep {} 45 | | ^^^^^^^^^^^ 46 | 47 | error[E0277]: `Subargs` is not a `palc::Subcommand` 48 | --> tests/ui/args-subcommand-non-subcommand.rs:14:10 49 | | 50 | 14 | cmd: Subargs, 51 | | ^^^^^^^ this type is expected to have `derive(palc::Subcommand)` but it is not 52 | | 53 | help: the trait `Subcommand` is not implemented for `Subargs` 54 | --> tests/ui/args-subcommand-non-subcommand.rs:18:1 55 | | 56 | 18 | struct Subargs {} 57 | | ^^^^^^^^^^^^^^ 58 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | 11 | env: 12 | RUSTFLAGS: -Dwarnings 13 | 14 | jobs: 15 | test: 16 | name: Rust ${{matrix.rust}} 17 | runs-on: ubuntu-latest 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | # NB. Sync with Cargo.toml 22 | rust: [nightly, beta, stable, 1.88] 23 | timeout-minutes: 15 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: dtolnay/rust-toolchain@master 27 | with: 28 | toolchain: ${{ matrix.rust }} 29 | # UI test outputs can include std code, which is unavailable without rust-src. 30 | # To make error messages consistent between CI and local tests, enable rust-src here. 31 | components: ${{ matrix.rust == 'stable' && 'rust-src' || '' }} 32 | - name: Enable type layout randomization 33 | run: echo RUSTFLAGS=${RUSTFLAGS}\ -Zrandomize-layout >> $GITHUB_ENV 34 | if: matrix.rust == 'nightly' 35 | - run: cargo test --all-targets 36 | - run: cargo test --all-targets --no-default-features 37 | - run: cargo test --test ui -- --ignored 38 | if: matrix.rust == 'stable' 39 | 40 | doc: 41 | name: Documentation 42 | runs-on: ubuntu-latest 43 | timeout-minutes: 15 44 | env: 45 | RUSTDOCFLAGS: -Dwarnings 46 | steps: 47 | - uses: actions/checkout@v4 48 | - uses: dtolnay/rust-toolchain@nightly 49 | - uses: dtolnay/install@cargo-docs-rs 50 | - run: cargo docs-rs 51 | 52 | clippy: 53 | name: Clippy 54 | runs-on: ubuntu-latest 55 | timeout-minutes: 15 56 | steps: 57 | - uses: actions/checkout@v4 58 | - uses: dtolnay/rust-toolchain@clippy 59 | - run: cargo clippy --all-targets --benches -- -Dclippy::all 60 | 61 | miri: 62 | name: Miri 63 | runs-on: ubuntu-latest 64 | timeout-minutes: 45 65 | steps: 66 | - uses: actions/checkout@v4 67 | - uses: dtolnay/rust-toolchain@miri 68 | with: 69 | toolchain: nightly 70 | - run: cargo miri setup 71 | - run: cargo miri test --all-targets 72 | env: 73 | MIRIFLAGS: -Zmiri-strict-provenance 74 | -------------------------------------------------------------------------------- /bench.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | filter="$1" 4 | shift 5 | 6 | die() { 7 | if [[ $# != 0 ]]; then 8 | echo "$*" >&2 9 | fi 10 | exit 1 11 | } 12 | 13 | getRequiredFeatures() { 14 | local name="$1" 15 | if [[ "$name" = *clap* ]]; then 16 | printf "%s" "--features clap " 17 | fi 18 | if [[ "$name" = *palc* ]]; then 19 | printf "%s" "--features palc " 20 | fi 21 | if [[ "$name" = *argh* ]]; then 22 | printf "%s" "--features argh " 23 | fi 24 | } 25 | 26 | getSize() { 27 | local -a cmd 28 | local out 29 | local name="$1" 30 | shift 31 | 32 | local cmd=( 33 | cargo bloated 34 | --package test-suite 35 | --bin "$name" 36 | --output sections 37 | --quiet \ 38 | $(getRequiredFeatures "$name") 39 | "$@" 40 | -- --quiet 41 | ) 42 | 43 | out="$("${cmd[@]}")" || die "command fail: ${cmd[*]}" 44 | sed -nE 's/.*\s(\S+)\s+\(file\).*/\1/p' <<<"$out" 45 | } 46 | 47 | getCompileTime() { 48 | local name="$1" 49 | shift 50 | 51 | local cmd=( 52 | cargo build 53 | --package test-suite 54 | --bin "$name" 55 | --quiet 56 | $(getRequiredFeatures "$name") 57 | "$@" 58 | ) 59 | # %E: elapsed time. 60 | CARGO_TARGET_DIR="$tmpdir" \ 61 | command time --format "%E" -- \ 62 | "${cmd[@]}" 2>&1 || die "failed to build" 63 | } 64 | 65 | 66 | tmpdir="$(mktemp -d /tmp/palc-target.XXXXXX)" || die "failed to mktemp" 67 | trap 'rm -r -- "$tmpdir"' EXIT 68 | 69 | # Prepare and download dependencies sources. 70 | cargo metadata --format-version 1 >/dev/null || die "failed to run cargo metadata" 71 | 72 | printf "%-20s %12s %12s %12s %12s\n" "name" "minimal" "default" "full-build" "incremental" 73 | 74 | for name in simple-{clap,argh,palc,none} criterion-{clap,argh,palc} deno-{clap,palc}; do 75 | if [[ "$name" != *"$filter"* ]]; then 76 | continue 77 | fi 78 | 79 | minSize="$(getSize $name)" || die 80 | defaultSize="$(getSize $name --features full-featured)" || die 81 | printf "%-20s %12s %12s" "$name" "$minSize" "$defaultSize" 82 | 83 | rm -rf "$tmpdir/debug" 84 | buildTime="$(getCompileTime "$name" --features full-featured)" || die 85 | incTime="$(getCompileTime "$name" --features full-featured)" || die 86 | printf "%12s %12s\n" "$buildTime" "$incTime" 87 | done 88 | -------------------------------------------------------------------------------- /tests/value_enum.rs: -------------------------------------------------------------------------------- 1 | use palc::__private::ValueEnum; 2 | use palc::{Parser, ValueEnum}; 3 | 4 | #[derive(Debug, ValueEnum)] 5 | enum Empty {} 6 | 7 | #[derive(Debug, PartialEq, ValueEnum)] 8 | enum Single { 9 | SingleVariant, 10 | } 11 | 12 | #[derive(Debug, PartialEq, ValueEnum)] 13 | #[value(rename_all = "camelCase")] 14 | enum Override { 15 | #[value(name = "")] 16 | A, 17 | DefaultVariant, 18 | #[value(name = "the value")] 19 | ExplicitVariant, 20 | } 21 | 22 | #[test] 23 | fn empty() { 24 | assert!(Empty::parse_value("").is_none()) 25 | } 26 | 27 | #[test] 28 | fn single() { 29 | assert_eq!(Single::parse_value("single-variant"), Some(Single::SingleVariant)); 30 | assert_eq!(Single::parse_value("Single-variant"), None); 31 | } 32 | 33 | #[test] 34 | fn rename_all() { 35 | assert_eq!(Override::parse_value(""), Some(Override::A)); 36 | assert_eq!(Override::parse_value("defaultVariant"), Some(Override::DefaultVariant)); 37 | assert_eq!(Override::parse_value("the value"), Some(Override::ExplicitVariant)); 38 | } 39 | 40 | macro_rules! test_renames { 41 | ($($convert:literal, $variant:ident, $expect:literal;)*) => { 42 | $( 43 | #[expect(non_snake_case)] 44 | #[test] 45 | fn $variant() { 46 | #[allow(non_camel_case_types)] 47 | #[derive(Debug, PartialEq, ValueEnum)] 48 | #[value(rename_all = $convert)] 49 | enum $variant { 50 | $variant, 51 | } 52 | 53 | assert_eq!($variant::parse_value($expect), Some($variant::$variant)); 54 | } 55 | )* 56 | }; 57 | } 58 | 59 | test_renames! { 60 | "camelCase", RenameCamel, "renameCamel"; 61 | "PascalCase", RenamePascal, "RenamePascal"; 62 | "SCREAMING_SNAKE_CASE", RenameScream, "RENAME_SCREAM"; 63 | "snake_case", RenameSnake, "rename_snake"; 64 | "lower", RenameLower, "renamelower"; 65 | "UPPER", RenameUpper, "RENAMEUPPER"; 66 | "verbatim", RenameXMLHttp_Request, "RenameXMLHttp_Request"; 67 | } 68 | 69 | #[test] 70 | fn ignore_case() { 71 | #[derive(Debug, PartialEq, ValueEnum)] 72 | enum Enum { 73 | HelloWorld, 74 | } 75 | 76 | #[derive(Debug, PartialEq, Parser)] 77 | struct Cli { 78 | #[arg(long, ignore_case = true)] 79 | key: Enum, 80 | } 81 | 82 | let got = Cli::try_parse_from(["", "--key", "hElLo-WoRlD"]).unwrap(); 83 | assert_eq!(got, Cli { key: Enum::HelloWorld }); 84 | } 85 | -------------------------------------------------------------------------------- /derive/src/error.rs: -------------------------------------------------------------------------------- 1 | //! A simplified version of proc-macro-error. 2 | //! 3 | //! Ref: 4 | use std::cell::Cell; 5 | 6 | use proc_macro2::{Span, TokenStream}; 7 | use quote::quote_spanned; 8 | 9 | thread_local! { 10 | static ERROR_TTS: Cell> = const { Cell::new(None) }; 11 | } 12 | 13 | // For unrecoverable errors. 14 | pub type Result = std::result::Result; 15 | 16 | macro_rules! emit_error { 17 | ($src:expr, $($tt:tt)+) => { 18 | crate::error::emit_error(syn::spanned::Spanned::span(&$src), &format!($($tt)+)) 19 | }; 20 | } 21 | 22 | macro_rules! abort { 23 | ($src:expr, $($tt:tt)+) => {{ 24 | emit_error!($src, $($tt)+); 25 | return Err(()); 26 | }}; 27 | } 28 | 29 | pub fn try_syn(ret: syn::Result) -> Option { 30 | match ret { 31 | Ok(v) => Some(v), 32 | Err(err) => { 33 | let mut tts = ERROR_TTS.take().unwrap_or_default(); 34 | tts.extend(err.to_compile_error()); 35 | ERROR_TTS.set(Some(tts)); 36 | None 37 | } 38 | } 39 | } 40 | 41 | pub fn emit_error(span: Span, msg: &str) { 42 | let mut tts = ERROR_TTS.take().unwrap_or_default(); 43 | tts.extend(quote_spanned! {span=> ::std::compile_error! { #msg } }); 44 | ERROR_TTS.set(Some(tts)); 45 | } 46 | 47 | pub fn catch_errors(f: impl FnOnce() -> Result) -> Result { 48 | struct Guard(Option); 49 | impl Drop for Guard { 50 | fn drop(&mut self) { 51 | ERROR_TTS.set(self.0.take()); 52 | } 53 | } 54 | 55 | let _guard = Guard(ERROR_TTS.replace(None)); 56 | let ret = f(); 57 | let errors = ERROR_TTS.take(); 58 | match (ret, errors) { 59 | (Ok(v), None) => Ok(v), 60 | (_, Some(err)) => { 61 | assert!(!err.is_empty()); 62 | Err(err) 63 | } 64 | (Err(()), None) => unreachable!(), 65 | } 66 | } 67 | 68 | // Hard error without fallback. Most of our traits requires `'static` 69 | // and cannot be implemented even in fallback impl. 70 | macro_rules! assert_no_generics { 71 | ($input:expr) => { 72 | if let Err(err) = crate::error::check_no_generics(&$input.generics) { 73 | return err; 74 | } 75 | }; 76 | } 77 | 78 | pub fn check_no_generics(generics: &syn::Generics) -> Result<(), TokenStream> { 79 | if generics.params.is_empty() 80 | && generics.where_clause.as_ref().is_none_or(|w| w.predicates.is_empty()) 81 | { 82 | Ok(()) 83 | } else { 84 | Err(syn::Error::new(Span::call_site(), "generics are not supported").into_compile_error()) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | //! Utilities used by but do not tied to runtime. 2 | 3 | #[cfg(feature = "help")] 4 | #[macro_export] 5 | #[doc(hidden)] 6 | macro_rules! __gate_help { 7 | ($disabled:expr, $($enabled:tt)*) => { 8 | $($enabled)* 9 | }; 10 | } 11 | 12 | #[cfg(not(feature = "help"))] 13 | #[macro_export] 14 | #[doc(hidden)] 15 | macro_rules! __gate_help { 16 | ($disabled:expr, $($enabled:tt)*) => { 17 | $disabled 18 | }; 19 | } 20 | 21 | #[macro_export] 22 | #[doc(hidden)] 23 | macro_rules! __const_concat { 24 | // Fast path for default `__raw_meta`. 25 | ($($s:literal,)* $(env!($e:literal), $($s2:literal,)*)*) => { 26 | $crate::__private::concat!($($s,)* $(env!($e), $($s2,)*)*) 27 | }; 28 | 29 | // Match exprs after literals to prevent invisible grouping. 30 | ($s:expr,) => { 31 | $s 32 | }; 33 | ($($s:expr,)*) => {{ 34 | const __STRS: &'static [&'static $crate::__private::str] = &[$($s),*]; 35 | match $crate::__private::from_utf8(&const { 36 | $crate::__private::const_concat_impl::<{ 37 | $crate::__private::const_concat_len(__STRS) 38 | }>(__STRS) 39 | }) { 40 | $crate::__private::Ok(__s) => __s, 41 | $crate::__private::Err(_) => $crate::__private::unreachable!(), 42 | } 43 | }}; 44 | } 45 | 46 | pub const fn const_concat_len(strs: &[&str]) -> usize { 47 | let mut ret = 0; 48 | let mut i = 0; 49 | let str_cnt = strs.len(); 50 | while i < str_cnt { 51 | ret += strs[i].len(); 52 | i += 1; 53 | } 54 | ret 55 | } 56 | 57 | pub const fn const_concat_impl(strs: &[&str]) -> [u8; LEN] { 58 | // Invalid UTF-8, to assert `LEN` is not too long. 59 | let mut buf = [0xFFu8; LEN]; 60 | let mut out: &mut [u8] = &mut buf; 61 | let mut i = 0; 62 | let str_cnt = strs.len(); 63 | while i < str_cnt { 64 | let s = strs[i].as_bytes(); 65 | let (lhs, rhs) = out.split_at_mut(s.len()); 66 | lhs.copy_from_slice(s); 67 | out = rhs; 68 | i += 1; 69 | } 70 | buf 71 | } 72 | 73 | // String splitting specialized for ASCII. 74 | 75 | #[inline(never)] 76 | pub(crate) fn split_once(s: &str, b: u8) -> Option<(&str, &str)> { 77 | assert!(b.is_ascii()); 78 | s.split_once(b as char) 79 | } 80 | 81 | pub(crate) fn split_sep_many(mut s: &str, b: u8) -> Option<[&str; N]> { 82 | assert!(b.is_ascii()); 83 | let mut arr = [""; N]; 84 | let (last, init) = arr.split_last_mut().unwrap(); 85 | for p in init { 86 | (*p, s) = split_once(s, b)?; 87 | } 88 | *last = s; 89 | Some(arr) 90 | } 91 | 92 | pub(crate) fn split_terminator(mut s: &str, b: u8) -> impl Iterator + Clone { 93 | assert!(b.is_ascii()); 94 | std::iter::from_fn(move || { 95 | let (fst, rest) = split_once(s, b)?; 96 | s = rest; 97 | Some(fst) 98 | }) 99 | } 100 | -------------------------------------------------------------------------------- /src/shared.rs: -------------------------------------------------------------------------------- 1 | //! NB. This file is shared between library and proc-macro crates. 2 | use std::num::NonZero; 3 | 4 | /// Bit-packed attribute of an logical argument. 5 | /// Used to carry additional information into the runtime. 6 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] 7 | pub struct ArgAttrs(pub u32); 8 | 9 | impl ArgAttrs { 10 | /// New from the field index of the containing struct implementing `Args`. 11 | /// 12 | /// It is at bit 0:7. 13 | /// 14 | /// NB: The proc-macro codegen relies on this being at the lowest 8-bits, by 15 | /// adding index offset directly into the underlying `u32`. 16 | pub const fn index(i: u8) -> Self { 17 | Self(i as u32) 18 | } 19 | 20 | #[must_use] 21 | pub fn get_index(self) -> u8 { 22 | self.0 as u8 23 | } 24 | 25 | /// New from an ASCII value delimiter. 26 | /// 27 | /// It is at bit 8:15. 28 | pub const fn delimiter(ch: Option>) -> Self { 29 | let ch = match ch { 30 | Some(ch) => ch.get(), 31 | None => 0, 32 | }; 33 | assert!(ch.is_ascii()); 34 | Self((ch as u32) << 8) 35 | } 36 | 37 | #[must_use] 38 | pub fn get_delimiter(self) -> Option> { 39 | NonZero::new((self.0 >> 8) as u8) 40 | } 41 | 42 | /// Does this argument require a value? This means it will consume the next 43 | /// argument as its value if no inlined value is provided. 44 | pub const REQUIRE_VALUE: Self = Self(1 << 16); 45 | /// Does this argument require an inlined value via `=`? 46 | pub const REQUIRE_EQ: Self = Self(1 << 17); 47 | /// Does this argument eat the next raw argument even if it starts with `-`? 48 | /// Only meaningful if [`Self::REQUIRE_VALUE`] is set. 49 | pub const ACCEPT_HYPHEN_ANY: Self = Self(1 << 18); 50 | /// Does this argument eat the next raw argument but only if it is a negative number? 51 | /// Only meaningful if [`Self::REQUIRE_VALUE`] is set. 52 | pub const ACCEPT_HYPHEN_NUM: Self = Self(1 << 19); 53 | /// Is this a global argument? 54 | pub const GLOBAL: Self = Self(1 << 20); 55 | /// Make the value lowercase before parsing it? 56 | pub const MAKE_LOWERCASE: Self = Self(1 << 21); 57 | /// Is this a greedy variable-length unnamed args that consumes everything after? 58 | pub const GREEDY: Self = Self(1 << 22); 59 | /// Is an inlined value provided? 60 | /// This flag is only set by the parser runtime to inform the place implementation. 61 | pub const HAS_INLINE_VALUE: Self = Self(1 << 23); 62 | 63 | pub fn set(&mut self, other: Self, value: bool) { 64 | if value { 65 | self.0 |= other.0; 66 | } 67 | // No `else`. This function is a conditional set and never unset. 68 | } 69 | 70 | #[must_use] 71 | pub const fn union(self, other: Self) -> Self { 72 | Self(self.0 | other.0) 73 | } 74 | 75 | #[must_use] 76 | pub const fn contains(self, other: Self) -> bool { 77 | self.0 & other.0 == other.0 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/help.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | refl::{FMT_NAMED, FMT_UNNAMED, FMT_USAGE_NAMED, FMT_USAGE_UNNAMED, RawArgsInfo}, 3 | runtime::ParserChainNode, 4 | }; 5 | 6 | #[inline(never)] 7 | fn push_str(out: &mut String, s: &str) { 8 | out.push_str(s); 9 | } 10 | 11 | fn collect_subcmds(out: &mut Vec<(String, &'static RawArgsInfo)>, chain: ParserChainNode) { 12 | let info = chain.state.info(); 13 | let cmd_name = chain.cmd_name.to_string_lossy().into_owned(); 14 | out.push((cmd_name, info)); 15 | if let Some(deep) = chain.ancestors.out() { 16 | collect_subcmds(out, deep); 17 | } 18 | } 19 | 20 | #[cold] 21 | pub(crate) fn render_help_into(out: &mut String, chain: &mut ParserChainNode) { 22 | macro_rules! w { 23 | ($($e:expr),*) => {{ 24 | $(push_str(out, $e);)* 25 | }}; 26 | } 27 | 28 | let path = { 29 | let mut path = Vec::with_capacity(8); 30 | collect_subcmds( 31 | &mut path, 32 | ParserChainNode { 33 | cmd_name: chain.cmd_name, 34 | state: chain.state, 35 | ancestors: chain.ancestors, 36 | }, 37 | ); 38 | path.reverse(); 39 | path 40 | }; 41 | 42 | // There must be at least a top-level `Parser` info, or we would fail fast by `MissingArg0`. 43 | assert!(!path.is_empty()); 44 | let info = path.last().unwrap().1; 45 | 46 | // About this (sub)command. 47 | let doc = info.doc(); 48 | if !doc.long_about.is_empty() { 49 | w!(doc.long_about, "\n\n"); 50 | } 51 | 52 | // Usage of current subcommand path. 53 | 54 | w!("Usage:"); 55 | // Argv0 is included. 56 | for (cmd, _) in path.iter() { 57 | w!(" ", cmd); 58 | } 59 | 60 | let fmt = |out: &mut String, what: u8| info.fmt_help(out, what); 61 | 62 | // TODO: Global args. 63 | fmt(out, FMT_USAGE_NAMED); 64 | if info.has_optional_named() { 65 | w!(" [OPTIONS]"); 66 | } 67 | fmt(out, FMT_USAGE_UNNAMED); 68 | 69 | let subcmds = info.subcommands(); 70 | if subcmds.is_some() { 71 | w!(if info.subcommand_optional() { " [COMMAND]" } else { " " }); 72 | } 73 | // EOL and empty line separator. 74 | w!("\n\n"); 75 | 76 | // List of commands. 77 | 78 | if let Some(subcmds) = subcmds { 79 | w!("Commands:\n"); 80 | let pad = " "; 81 | let max_len = subcmds.clone().map(|(cmd, _)| cmd.len()).max().unwrap_or(0); 82 | 83 | // Note: Only short help is displayed for the subcommand list. 84 | for (cmd, long_about) in subcmds { 85 | w!(" ", cmd); 86 | if !long_about.is_empty() { 87 | let short_about = long_about.split_terminator('\n').next().unwrap_or(long_about); 88 | let pad_len = max_len.saturating_sub(cmd.len()) + 2; 89 | let pad = &pad[..pad.len().min(pad_len)]; 90 | w!("", pad, short_about); 91 | } 92 | w!("\n"); 93 | } 94 | 95 | // Empty line separator. 96 | w!("\n"); 97 | } 98 | 99 | // List of unnamed arguments. 100 | { 101 | let last = out.len(); 102 | w!("Arguments:\n"); 103 | let banner = out.len(); 104 | fmt(out, FMT_UNNAMED); 105 | if out.len() == banner { 106 | out.truncate(last); 107 | } 108 | // Empty line separator should already be emitted. 109 | } 110 | 111 | // List of named arguments. 112 | { 113 | let last = out.len(); 114 | w!("Options:\n"); 115 | let banner = out.len(); 116 | fmt(out, FMT_NAMED); 117 | if out.len() == banner { 118 | out.truncate(last); 119 | } 120 | // Empty line separator should already be emitted. 121 | } 122 | 123 | w!(doc.after_long_help); 124 | } 125 | -------------------------------------------------------------------------------- /tests/help.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "help")] 2 | #![expect(dead_code, reason = "only for help generation")] 3 | use expect_test::{Expect, expect}; 4 | use palc::{Args, Parser, Subcommand}; 5 | 6 | /// My great app. 7 | #[derive(Parser)] 8 | struct ArgsCli { 9 | /// Log more details. 10 | #[arg(short = 'v')] 11 | verbose: bool, 12 | 13 | #[command(subcommand)] 14 | command: Commands, 15 | } 16 | 17 | #[derive(Subcommand)] 18 | enum Commands { 19 | /// Run the app. 20 | Run { 21 | /// Passthru arguments. 22 | #[arg(last = true)] 23 | args: Vec, 24 | }, 25 | /// Run some tests. 26 | Test { 27 | /// Filter patterns. 28 | filters: Vec, 29 | #[arg(long)] 30 | feature: Vec, 31 | }, 32 | Config(ConfigCli), 33 | } 34 | 35 | /// Configure something. 36 | /// 37 | /// Some detailed explanation. 38 | #[derive(Args)] 39 | #[command(after_long_help = "this command provides absolutely no warranty!")] 40 | struct ConfigCli { 41 | #[arg(short, long, value_name = "VALUE")] 42 | key: String, 43 | 44 | /// Enable debug. 45 | #[arg(long)] 46 | debug: bool, 47 | 48 | /// Force to do it. 49 | #[arg(short)] 50 | force: bool, 51 | } 52 | 53 | #[track_caller] 54 | fn assert_help(args: &[&str], expect: Expect) { 55 | let help = P::try_parse_from(args).err().unwrap().try_into_help().unwrap(); 56 | expect.assert_eq(&help); 57 | } 58 | 59 | #[test] 60 | fn top_level() { 61 | assert_help::( 62 | &["me", "--help"], 63 | expect![[r#" 64 | My great app. 65 | 66 | Usage: me [OPTIONS] 67 | 68 | Commands: 69 | run Run the app. 70 | test Run some tests. 71 | config Configure something. 72 | 73 | Options: 74 | -v 75 | Log more details. 76 | 77 | "#]], 78 | ); 79 | } 80 | 81 | #[test] 82 | fn subcommand() { 83 | assert_help::( 84 | &["me", "config", "--help"], 85 | expect![[r#" 86 | Configure something. 87 | Some detailed explanation. 88 | 89 | Usage: me config --key [OPTIONS] 90 | 91 | Options: 92 | -k, --key 93 | 94 | --debug 95 | Enable debug. 96 | 97 | -f 98 | Force to do it. 99 | 100 | this command provides absolutely no warranty!"#]], 101 | ); 102 | } 103 | 104 | #[test] 105 | fn last() { 106 | assert_help::( 107 | &["me", "run", "--help"], 108 | expect![[r#" 109 | Run the app. 110 | 111 | Usage: me run -- [ARGS]... 112 | 113 | Arguments: 114 | [ARGS]... 115 | Passthru arguments. 116 | 117 | "#]], 118 | ); 119 | } 120 | 121 | #[test] 122 | fn multiple() { 123 | assert_help::( 124 | &["me", "test", "--help"], 125 | expect![[r#" 126 | Run some tests. 127 | 128 | Usage: me test [OPTIONS] [FILTERS]... 129 | 130 | Arguments: 131 | [FILTERS]... 132 | Filter patterns. 133 | 134 | Options: 135 | --feature 136 | 137 | "#]], 138 | ); 139 | } 140 | 141 | #[test] 142 | fn default_value() { 143 | #[derive(Parser)] 144 | struct Cli { 145 | #[arg(long, default_value = "invalid")] 146 | default_str: i32, 147 | #[arg(long, default_value_t)] 148 | default_trait: i32, 149 | /// Preferred! 150 | #[arg(long, default_value_t = 42)] 151 | default_expr: i32, 152 | #[arg(long, default_value_t = "foo".into())] 153 | infer: String, 154 | #[arg(default_value = "static")] 155 | unnamed: Option, 156 | } 157 | 158 | assert_help::( 159 | &["me", "--help"], 160 | expect![[r#" 161 | Usage: me [OPTIONS] 162 | 163 | Arguments: 164 | [UNNAMED] 165 | [default: static] 166 | 167 | Options: 168 | --default-str 169 | [default: invalid] 170 | 171 | --default-trait 172 | [default: 0] 173 | 174 | --default-expr 175 | Preferred! 176 | [default: 42] 177 | 178 | --infer 179 | [default: foo] 180 | 181 | "#]], 182 | ); 183 | } 184 | -------------------------------------------------------------------------------- /derive/src/derive_value_enum.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use quote::{ToTokens, quote}; 3 | use syn::{Data, DataEnum, DeriveInput, Generics, Ident}; 4 | 5 | use crate::{ 6 | common::{ValueEnumMeta, ValueVariantMeta, wrap_anon_item}, 7 | error::catch_errors, 8 | }; 9 | 10 | pub(crate) fn expand(input: &DeriveInput) -> TokenStream { 11 | match catch_errors(|| match &input.data { 12 | Data::Enum(data) => Ok(expand_for_enum(input, data)), 13 | _ => abort!(Span::call_site(), "only enums are supported"), 14 | }) { 15 | Ok(tts) => wrap_anon_item(tts), 16 | Err(mut tts) => { 17 | let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); 18 | let name = &input.ident; 19 | 20 | tts.extend(wrap_anon_item(quote! { 21 | #[automatically_derived] 22 | impl #impl_generics __rt::ValueEnum for #name #ty_generics #where_clause {} 23 | 24 | #[automatically_derived] 25 | impl #impl_generics __rt::fmt::Display for #name #ty_generics #where_clause { 26 | fn fmt(&self, _: &mut __rt::fmt::Formatter<'_>) -> __rt::fmt::Result { 27 | __rt::unimplemented!() 28 | } 29 | } 30 | })); 31 | tts 32 | } 33 | } 34 | } 35 | 36 | fn expand_for_enum<'a>(input: &'a DeriveInput, data: &'a DataEnum) -> ValueEnumImpl<'a> { 37 | let enum_meta = ValueEnumMeta::parse_attrs(&input.attrs); 38 | 39 | let mut variants = data 40 | .variants 41 | .iter() 42 | .filter_map(|variant| { 43 | if !matches!(variant.fields, syn::Fields::Unit) { 44 | emit_error!(variant.ident, "only unit variant is supported"); 45 | return None; 46 | } 47 | let variant_meta = ValueVariantMeta::parse_attrs(&variant.attrs); 48 | 49 | let parse_name = match variant_meta.name { 50 | Some(name) => name, 51 | None => enum_meta.rename_all.rename(variant.ident.to_string()), 52 | }; 53 | 54 | if parse_name.contains(|c: char| c.is_ascii_control()) { 55 | emit_error!( 56 | variant.ident, 57 | "value names containing ASCII control characters are not supported", 58 | ); 59 | return None; 60 | } 61 | 62 | Some(Variant { parse_name, ident: &variant.ident }) 63 | }) 64 | .collect::>(); 65 | 66 | variants.sort_by(|lhs, rhs| Ord::cmp(&lhs.parse_name, &rhs.parse_name)); 67 | if let Some(w) = variants.windows(2).find(|w| w[0].parse_name == w[1].parse_name) { 68 | emit_error!(w[0].ident, "duplicated possible values {:?}", w[0].parse_name); 69 | emit_error!(w[1].ident.span(), "second variant here"); 70 | } 71 | 72 | ValueEnumImpl { ident: &input.ident, generics: &input.generics, variants } 73 | } 74 | 75 | struct ValueEnumImpl<'i> { 76 | ident: &'i Ident, 77 | generics: &'i Generics, 78 | variants: Vec>, 79 | } 80 | 81 | struct Variant<'i> { 82 | parse_name: String, 83 | ident: &'i Ident, 84 | } 85 | 86 | impl ToTokens for ValueEnumImpl<'_> { 87 | fn to_tokens(&self, tokens: &mut TokenStream) { 88 | let name = self.ident; 89 | let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl(); 90 | let variant_strs = self.variants.iter().map(|v| &v.parse_name); 91 | let variant_idents = self.variants.iter().map(|v| v.ident); 92 | let variant_strs2 = variant_strs.clone(); 93 | let variant_idents2 = variant_idents.clone(); 94 | 95 | let possible_inputs_nul = 96 | self.variants.iter().flat_map(|v| [&v.parse_name, "\0"]).collect::(); 97 | 98 | let no_upper_case = 99 | self.variants.iter().all(|v| v.parse_name.bytes().all(|b| !b.is_ascii_uppercase())); 100 | 101 | tokens.extend(quote! { 102 | #[automatically_derived] 103 | impl #impl_generics __rt::ValueEnum for #name #ty_generics #where_clause { 104 | const POSSIBLE_INPUTS_NUL: &'static __rt::str = #possible_inputs_nul; 105 | const NO_UPPER_CASE: __rt::bool = #no_upper_case; 106 | 107 | // If there is no variant. 108 | #[allow(unreachable_code)] 109 | fn parse_value(__v: &__rt::str) -> __rt::Option { 110 | __rt::Some(match __v { 111 | #(#variant_strs => Self:: #variant_idents,)* 112 | _ => return __rt::None 113 | }) 114 | } 115 | } 116 | 117 | #[automatically_derived] 118 | impl #impl_generics __rt::fmt::Display for #name #ty_generics #where_clause { 119 | // If there is no variant. 120 | #[allow(unreachable_code)] 121 | fn fmt(&self, __f: &mut __rt::fmt::Formatter<'_>) -> __rt::fmt::Result { 122 | __f.write_str(match *self { 123 | #(Self:: #variant_idents2 => #variant_strs2,)* 124 | }) 125 | } 126 | } 127 | }); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/values.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsStr; 2 | use std::marker::PhantomData; 3 | use std::ops::Deref; 4 | use std::str::FromStr; 5 | 6 | use crate::error::DynStdError; 7 | use crate::{ErrorKind, Result}; 8 | 9 | mod sealed { 10 | pub trait Sealed {} 11 | } 12 | 13 | /// Value types that are parsable from raw `&OsStr`. 14 | /// 15 | /// It behaves like a `clap::ValueParser` but in type-level. 16 | #[diagnostic::on_unimplemented( 17 | message = "`{Self}` cannot be parsed into a palc argument value", 18 | label = "unparsable value type", 19 | note = "this is an internal trait that should NOT be implemented directly", 20 | note = "types are parsable by `derive(palc::ValueEnum)`, or by implementing `TryFrom<&OsStr>`, 21 | `From<&OsStr>` or `FromStr`, with error type be either `&str`, `String` or \ 22 | `impl std::error::Error + Send + Sync`" 23 | )] 24 | pub trait ValueParser: Sized + sealed::Sealed + 'static { 25 | type Output: Sized; 26 | 27 | /// Possible input strings terminated by NUL. 28 | const POSSIBLE_INPUTS_NUL: &'static str = ""; 29 | 30 | fn parse(v: &OsStr) -> Result; 31 | } 32 | 33 | /// This trait definition is not a public API, only the derive-macro is. 34 | #[doc(hidden)] 35 | pub trait ValueEnum: std::fmt::Display + Sized { 36 | /// See [`ValueParser::POSSIBLE_INPUTS_NUL`]. 37 | const POSSIBLE_INPUTS_NUL: &'static str = ""; 38 | 39 | /// Whether there is no ASCII upper case letter in any variants. 40 | /// This is required for `arg(ignore_case)`. 41 | const NO_UPPER_CASE: bool = true; 42 | 43 | fn parse_value(_s: &str) -> Option { 44 | None 45 | } 46 | } 47 | 48 | pub struct InferValueParser(pub PhantomData<(T, Fuel)>); 49 | 50 | impl Deref for InferValueParser { 51 | type Target = InferValueParser; 52 | fn deref(&self) -> &Self::Target { 53 | &InferValueParser(PhantomData) 54 | } 55 | } 56 | 57 | // This function is displayed in error message, thus describes itself in its name. 58 | 59 | pub fn assert_auto_infer_value_parser_ok(p: P) -> P { 60 | p 61 | } 62 | 63 | // Level 3 64 | 65 | impl InferValueParser { 66 | pub fn get(&self) -> impl ValueParser { 67 | ValueEnumParser(PhantomData) 68 | } 69 | } 70 | struct ValueEnumParser(PhantomData); 71 | impl sealed::Sealed for ValueEnumParser {} 72 | impl ValueParser for ValueEnumParser { 73 | type Output = T; 74 | const POSSIBLE_INPUTS_NUL: &'static str = T::POSSIBLE_INPUTS_NUL; 75 | 76 | fn parse(v: &OsStr) -> Result { 77 | // TODO: better diagnostics? 78 | v.to_str() 79 | .ok_or(ErrorKind::InvalidUtf8) 80 | .and_then(|s| T::parse_value(s).ok_or(ErrorKind::InvalidValue)) 81 | .map_err(|err| { 82 | err.with_input(v.to_owned()).with_possible_values(T::POSSIBLE_INPUTS_NUL) 83 | }) 84 | } 85 | } 86 | 87 | // Level 2 88 | 89 | impl InferValueParser 90 | where 91 | T: for<'a> TryFrom<&'a OsStr, Error: Into> + 'static, 92 | { 93 | pub fn get(&self) -> impl ValueParser { 94 | TryFromOsStrParser(PhantomData) 95 | } 96 | } 97 | struct TryFromOsStrParser(PhantomData); 98 | impl sealed::Sealed for TryFromOsStrParser {} 99 | impl ValueParser for TryFromOsStrParser 100 | where 101 | T: for<'a> TryFrom<&'a OsStr, Error: Into> + 'static, 102 | { 103 | type Output = T; 104 | fn parse(v: &OsStr) -> Result { 105 | T::try_from(v) 106 | .map_err(|err| ErrorKind::InvalidValue.with_input(v.into()).with_source(err.into())) 107 | } 108 | } 109 | 110 | // Level 1 111 | 112 | impl InferValueParser 113 | where 114 | T: FromStr> + 'static, 115 | { 116 | pub fn get(&self) -> impl ValueParser { 117 | FromStrParser(PhantomData) 118 | } 119 | } 120 | struct FromStrParser(PhantomData); 121 | impl sealed::Sealed for FromStrParser {} 122 | impl ValueParser for FromStrParser 123 | where 124 | T: FromStr> + 'static, 125 | { 126 | type Output = T; 127 | fn parse(v: &OsStr) -> Result { 128 | let s = v.to_str().ok_or_else(|| ErrorKind::InvalidUtf8.with_input(v.into()))?; 129 | let t = s 130 | .parse::() 131 | .map_err(|err| ErrorKind::InvalidValue.with_input(s.into()).with_source(err.into()))?; 132 | Ok(t) 133 | } 134 | } 135 | 136 | // Level 0 137 | 138 | // For error reporting. 139 | // Since `ValueParser` is sealed and all implementations are private, this user type is guaranteed 140 | // to cause an unimplemented error on `ValueParser`. 141 | impl InferValueParser { 142 | pub fn get(&self) -> T { 143 | unreachable!() 144 | } 145 | } 146 | 147 | #[test] 148 | fn infer_value_parser() { 149 | use std::ffi::OsString; 150 | use std::path::PathBuf; 151 | 152 | macro_rules! infer { 153 | ($ty:ty) => { 154 | InferValueParser::<$ty, &&&()>(PhantomData).get() 155 | }; 156 | } 157 | 158 | fn has_parser(_: impl ValueParser) {} 159 | 160 | enum MyValueEnum {} 161 | impl std::fmt::Display for MyValueEnum { 162 | fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 163 | unreachable!() 164 | } 165 | } 166 | impl ValueEnum for MyValueEnum { 167 | fn parse_value(_s: &str) -> Option { 168 | None 169 | } 170 | } 171 | 172 | has_parser::(infer!(MyValueEnum)); 173 | has_parser::(infer!(OsString)); 174 | has_parser::(infer!(PathBuf)); 175 | has_parser::(infer!(String)); 176 | has_parser::(infer!(usize)); 177 | 178 | // `unreachable!()` at runtime. 179 | let _ = || { 180 | struct MyType; 181 | 182 | let () = infer!(()); 183 | let _: MyType = infer!(MyType); 184 | }; 185 | } 186 | -------------------------------------------------------------------------------- /tests/ui/args-type-unparsable.stderr: -------------------------------------------------------------------------------- 1 | error[E0277]: `MyType` cannot be parsed into a palc argument value 2 | --> tests/ui/args-type-unparsable.rs:5:10 3 | | 4 | 5 | arg: MyType, 5 | | ^^^^^^ unparsable value type 6 | | 7 | help: the trait `ValueParser` is not implemented for `MyType` 8 | --> tests/ui/args-type-unparsable.rs:19:1 9 | | 10 | 19 | struct MyType; 11 | | ^^^^^^^^^^^^^ 12 | = note: this is an internal trait that should NOT be implemented directly 13 | = note: types are parsable by `derive(palc::ValueEnum)`, or by implementing `TryFrom<&OsStr>`, 14 | `From<&OsStr>` or `FromStr`, with error type be either `&str`, `String` or `impl std::error::Error + Send + Sync` 15 | note: required by a bound in `assert_auto_infer_value_parser_ok` 16 | --> src/values.rs 17 | | 18 | | pub fn assert_auto_infer_value_parser_ok(p: P) -> P { 19 | | ^^^^^^^^^^^ required by this bound in `assert_auto_infer_value_parser_ok` 20 | 21 | error[E0277]: `MyType` cannot be parsed into a palc argument value 22 | --> tests/ui/args-type-unparsable.rs:9:18 23 | | 24 | 9 | vec_arg: Vec, 25 | | ^^^^^^ unparsable value type 26 | | 27 | help: the trait `ValueParser` is not implemented for `MyType` 28 | --> tests/ui/args-type-unparsable.rs:19:1 29 | | 30 | 19 | struct MyType; 31 | | ^^^^^^^^^^^^^ 32 | = note: this is an internal trait that should NOT be implemented directly 33 | = note: types are parsable by `derive(palc::ValueEnum)`, or by implementing `TryFrom<&OsStr>`, 34 | `From<&OsStr>` or `FromStr`, with error type be either `&str`, `String` or `impl std::error::Error + Send + Sync` 35 | note: required by a bound in `assert_auto_infer_value_parser_ok` 36 | --> src/values.rs 37 | | 38 | | pub fn assert_auto_infer_value_parser_ok(p: P) -> P { 39 | | ^^^^^^^^^^^ required by this bound in `assert_auto_infer_value_parser_ok` 40 | 41 | error[E0277]: `MyType` cannot be parsed into a palc argument value 42 | --> tests/ui/args-type-unparsable.rs:3:17 43 | | 44 | 3 | positional: MyType, 45 | | ^^^^^^ unparsable value type 46 | | 47 | help: the trait `ValueParser` is not implemented for `MyType` 48 | --> tests/ui/args-type-unparsable.rs:19:1 49 | | 50 | 19 | struct MyType; 51 | | ^^^^^^^^^^^^^ 52 | = note: this is an internal trait that should NOT be implemented directly 53 | = note: types are parsable by `derive(palc::ValueEnum)`, or by implementing `TryFrom<&OsStr>`, 54 | `From<&OsStr>` or `FromStr`, with error type be either `&str`, `String` or `impl std::error::Error + Send + Sync` 55 | note: required by a bound in `assert_auto_infer_value_parser_ok` 56 | --> src/values.rs 57 | | 58 | | pub fn assert_auto_infer_value_parser_ok(p: P) -> P { 59 | | ^^^^^^^^^^^ required by this bound in `assert_auto_infer_value_parser_ok` 60 | 61 | error[E0277]: `MyType` cannot be parsed into a palc argument value 62 | --> tests/ui/args-type-unparsable.rs:7:14 63 | | 64 | 7 | vec: Vec, 65 | | ^^^^^^ unparsable value type 66 | | 67 | help: the trait `ValueParser` is not implemented for `MyType` 68 | --> tests/ui/args-type-unparsable.rs:19:1 69 | | 70 | 19 | struct MyType; 71 | | ^^^^^^^^^^^^^ 72 | = note: this is an internal trait that should NOT be implemented directly 73 | = note: types are parsable by `derive(palc::ValueEnum)`, or by implementing `TryFrom<&OsStr>`, 74 | `From<&OsStr>` or `FromStr`, with error type be either `&str`, `String` or `impl std::error::Error + Send + Sync` 75 | note: required by a bound in `assert_auto_infer_value_parser_ok` 76 | --> src/values.rs 77 | | 78 | | pub fn assert_auto_infer_value_parser_ok(p: P) -> P { 79 | | ^^^^^^^^^^^ required by this bound in `assert_auto_infer_value_parser_ok` 80 | 81 | error[E0277]: `MyType` cannot be parsed into a palc argument value 82 | --> tests/ui/args-type-unparsable.rs:16:32 83 | | 84 | 16 | option_vec_arg: Option>, 85 | | ^^^^^^ unparsable value type 86 | | 87 | help: the trait `ValueParser` is not implemented for `MyType` 88 | --> tests/ui/args-type-unparsable.rs:19:1 89 | | 90 | 19 | struct MyType; 91 | | ^^^^^^^^^^^^^ 92 | = note: this is an internal trait that should NOT be implemented directly 93 | = note: types are parsable by `derive(palc::ValueEnum)`, or by implementing `TryFrom<&OsStr>`, 94 | `From<&OsStr>` or `FromStr`, with error type be either `&str`, `String` or `impl std::error::Error + Send + Sync` 95 | note: required by a bound in `assert_auto_infer_value_parser_ok` 96 | --> src/values.rs 97 | | 98 | | pub fn assert_auto_infer_value_parser_ok(p: P) -> P { 99 | | ^^^^^^^^^^^ required by this bound in `assert_auto_infer_value_parser_ok` 100 | 101 | error[E0277]: `MyType` cannot be parsed into a palc argument value 102 | --> tests/ui/args-type-unparsable.rs:14:28 103 | | 104 | 14 | option_vec: Option>, 105 | | ^^^^^^ unparsable value type 106 | | 107 | help: the trait `ValueParser` is not implemented for `MyType` 108 | --> tests/ui/args-type-unparsable.rs:19:1 109 | | 110 | 19 | struct MyType; 111 | | ^^^^^^^^^^^^^ 112 | = note: this is an internal trait that should NOT be implemented directly 113 | = note: types are parsable by `derive(palc::ValueEnum)`, or by implementing `TryFrom<&OsStr>`, 114 | `From<&OsStr>` or `FromStr`, with error type be either `&str`, `String` or `impl std::error::Error + Send + Sync` 115 | note: required by a bound in `assert_auto_infer_value_parser_ok` 116 | --> src/values.rs 117 | | 118 | | pub fn assert_auto_infer_value_parser_ok(p: P) -> P { 119 | | ^^^^^^^^^^^ required by this bound in `assert_auto_infer_value_parser_ok` 120 | -------------------------------------------------------------------------------- /test-suite/src/bin/common/criterion.rs: -------------------------------------------------------------------------------- 1 | //! This is an example for tons of arguments but no groups or subcommands. 2 | //! 3 | //! Manually translated from: 4 | //! 5 | //! 6 | //! Criterion.rs is dual licensed under the Apache 2.0 license and the MIT license. 7 | //! See more at 8 | use std::num::NonZero; 9 | 10 | use super::{Parser, ValueEnum}; 11 | 12 | #[derive(Parser)] 13 | #[command( 14 | name = "Criterion Benchmark", 15 | after_long_help = " 16 | This executable is a Criterion.rs benchmark. 17 | See https://github.com/bheisler/criterion.rs for more details. 18 | 19 | To enable debug output, define the environment variable CRITERION_DEBUG. 20 | Criterion.rs will output more debug information and will save the gnuplot 21 | scripts alongside the generated plots. 22 | 23 | To test that the benchmarks work, run `cargo test --benches` 24 | 25 | NOTE: If you see an 'unrecognized option' error using any of the options above, see: 26 | https://bheisler.github.io/criterion.rs/book/faq.html 27 | " 28 | )] 29 | pub struct Cli { 30 | /// Skip benchmarks whose names do not contain FILTER. 31 | filter: Vec, 32 | /// Configure coloring of output. always = always colorize output, never = never colorize 33 | /// output, auto = colorize output if output is a tty and compiled for unix. 34 | #[arg(short, long, alias = "colour", value_enum, default_value_t)] 35 | color: Color, 36 | /// Print additional statistical information. 37 | #[arg(short, long)] 38 | verbose: bool, 39 | /// Print only the benchmark results. 40 | #[arg(short, long, conflicts_with = "verbose")] 41 | quiet: bool, 42 | /// Disable plut and HTML generation. 43 | #[arg(short, long)] 44 | noplot: bool, 45 | /// Save results under a named baseline. 46 | #[arg(short, long, default_value = "base")] 47 | save_baseline: String, 48 | /// Discard benchmark results. 49 | #[arg(long, conflicts_with_all = ["save_baseline", "baseline", "baseline_lenient"])] 50 | discard_baseline: bool, 51 | /// Compare to a named baseline. If any benchmarks do not have the specified baseline this 52 | /// command fails. 53 | #[arg(short, long, conflicts_with_all = ["save_baseline", "baseline_lenient"])] 54 | baseline: Option, 55 | #[arg(long, conflicts_with_all = ["save_baseline", "baseline"])] 56 | baseline_lenient: Option, 57 | /// List all benchmarks 58 | #[arg(long, conflicts_with_all = ["test", "profile_time"])] 59 | list: bool, 60 | /// Output formatting 61 | #[arg(long, value_enum, default_value_t)] 62 | format: Format, 63 | /// List or run ignored benchmarks (currently means skip all benchmarks) 64 | #[arg(long)] 65 | ignored: bool, 66 | /// Run benchmarks that exactly match the provided filter 67 | #[arg(long)] 68 | exact: bool, 69 | /// Iterate each benchmark for approximately the given number of seconds, doing no analysis and 70 | /// without storing the results. Useful for running the benchmarks in a profiler. 71 | #[arg(long, conflicts_with_all = ["test", "list"])] 72 | profile_time: Option, 73 | /// Load a previous baseline instead of sampling new data. 74 | #[arg(long, conflicts_with = "profile_time", requires = "baseline")] 75 | load_baseline: Option, 76 | /// Changes the default size of the sample for this run. 77 | #[arg(long, default_value_t = 100)] 78 | sample_size: usize, 79 | /// Changes the default warm up time for this run. 80 | #[arg(long, default_value_t = 3.0)] 81 | warm_up_time: f64, 82 | /// Changes the default measurement time for this run. 83 | #[arg(long, default_value_t = 5.0)] 84 | measurement_time: f64, 85 | /// Changes the default number of resamples for this run. 86 | #[arg(long, default_value_t = 100000.try_into().unwrap())] 87 | nresamples: NonZero, 88 | /// Changes the default noise threshold for this run. 89 | #[arg(long, default_value_t = 0.01)] 90 | noise_threshld: f64, 91 | /// Changes the default confidence level for this run. 92 | #[arg(long, default_value_t = 0.95)] 93 | confidence_level: f64, 94 | /// Changes the default significance level for this run. 95 | #[arg(long, default_value_t = 0.05)] 96 | significance_level: f64, 97 | /// Benchmark only until the significance level has been reached 98 | #[arg(long, conflicts_with = "sample_size")] 99 | quick: bool, 100 | /// Run the benchmarks once, to verify that they execute successfully, but do not measure or 101 | /// report the results. 102 | #[arg(long, hide = true, conflicts_with_all = ["list", "profile_time"])] 103 | test: bool, 104 | #[arg(long, hide = true)] 105 | bench: Option, 106 | /// Set the plotting backend. By default, Criterion.rs will use the gnuplot backend if gnuplot 107 | /// is available, or the plotters backend if it isn't. 108 | #[arg(long)] 109 | plotting_backend: Option, 110 | /// Change the CLI output format. By default, Criterion.rs will use its own format. If output 111 | /// format is set to 'bencher', Criterion.rs will print output in a format that resembles the 112 | /// 'bencher' crate. 113 | #[arg(long, value_enum, default_value_t)] 114 | output_format: OutputFormat, 115 | /// Ignored, but added for compatibility with libtest. 116 | #[arg(long, hide = true)] 117 | nocapture: bool, 118 | /// Ignored, but added for compatibility with libtest. 119 | #[arg(long, hide = true)] 120 | show_output: bool, 121 | /// Ignored, but added for compatibility with libtest. 122 | #[arg(long, hide = true)] 123 | include_ignored: bool, 124 | #[arg(long, short = 'V', hide = true)] 125 | version: bool, 126 | } 127 | 128 | #[derive(Default, Clone, ValueEnum)] 129 | enum Color { 130 | #[default] 131 | Auto, 132 | Always, 133 | Never, 134 | } 135 | 136 | #[derive(Default, Clone, ValueEnum)] 137 | enum Format { 138 | #[default] 139 | Pretty, 140 | Terse, 141 | } 142 | 143 | #[derive(Clone, ValueEnum)] 144 | enum PlottingBackend { 145 | Gnuplot, 146 | Plotters, 147 | } 148 | 149 | #[derive(Default, Clone, ValueEnum)] 150 | enum OutputFormat { 151 | #[default] 152 | Criterion, 153 | Test, 154 | } 155 | -------------------------------------------------------------------------------- /src/refl.rs: -------------------------------------------------------------------------------- 1 | //! Runtime reflection of arguments, subcommands and help strings. 2 | //! Required by precise error messages and help generations. 3 | //! 4 | //! Most of `&str` here can be changed to thin `&CStr`, which is blocked by extern types. 5 | //! WAIT: 6 | //! 7 | //! TODO(low): Decide whether to expose these API. 8 | #![cfg_attr(not(feature = "default"), allow(unused))] 9 | 10 | use std::fmt; 11 | #[cfg(not(feature = "help"))] 12 | use std::marker::PhantomData; 13 | 14 | use crate::util::{split_once, split_sep_many, split_terminator}; 15 | 16 | /// Runtime information of a enum of subcommands. 17 | #[derive(Debug)] 18 | pub struct RawSubcommandInfo { 19 | /// Zero or more NUL-terminated subcommand names. 20 | #[cfg(feature = "help")] 21 | subcommands: &'static str, 22 | 23 | /// `RawArgsInfo::cmd_doc` of each subcommand. 24 | #[cfg(feature = "help")] 25 | cmd_docs: A, 26 | 27 | #[cfg(not(feature = "help"))] 28 | _marker: PhantomData, 29 | } 30 | 31 | impl RawSubcommandInfo { 32 | pub(crate) const EMPTY_REF: &Self = &Self::new("", []); 33 | 34 | // Used by proc-macro. 35 | #[cfg(feature = "help")] 36 | pub const fn new( 37 | subcommands: &'static str, 38 | cmd_docs: [&'static str; N], 39 | ) -> RawSubcommandInfo<[&'static str; N]> { 40 | RawSubcommandInfo { subcommands, cmd_docs } 41 | } 42 | 43 | #[cfg(not(feature = "help"))] 44 | pub const fn new( 45 | _subcommands: &'static str, 46 | _cmd_docs: [&'static str; N], 47 | ) -> Self { 48 | Self { _marker: PhantomData } 49 | } 50 | } 51 | 52 | /// - `w` will always be a `&mut String` but type-erased to avoid aggressive 53 | /// inlining (reserve, fail handling, inlined memcpy). 54 | /// - `what` indicates what to format, see constants below. 55 | type FmtWriter = fn(w: &mut dyn fmt::Write, what: u8); 56 | 57 | const fn fmt_noop(_w: &mut dyn fmt::Write, _what: u8) {} 58 | 59 | // This should be an enum but we use numbers to simplify proc-macro codegen. 60 | pub(crate) const FMT_UNNAMED: u8 = 0; 61 | pub(crate) const FMT_NAMED: u8 = 1; 62 | pub(crate) const FMT_USAGE_UNNAMED: u8 = 2; 63 | pub(crate) const FMT_USAGE_NAMED: u8 = 3; 64 | 65 | // Break the type cycle. 66 | #[derive(Debug, Clone, Copy)] 67 | pub struct RawArgsInfoRef(pub &'static RawArgsInfo); 68 | 69 | #[derive(Debug)] 70 | pub struct RawArgsInfo { 71 | /// Zero or more '\0'-terminated argument descriptions, either: 72 | /// `-s`, `--long`, `-s, --long=`, ``, or `[OPTIONAL]`. 73 | descriptions: &'static str, 74 | 75 | /// Is the child subcommand optional or required? Only useful if there are subcommands. 76 | #[cfg(feature = "help")] 77 | subcmd_optional: bool, 78 | 79 | /// If there is any optional named args, so that "[OPTIONS]" should be shown? 80 | #[cfg(feature = "help")] 81 | has_optional_named: bool, 82 | 83 | /// Child subcommands. 84 | #[cfg(feature = "help")] 85 | subcmd_info: Option<&'static RawSubcommandInfo>, 86 | 87 | /// The documentation about this command applet. 88 | /// 89 | /// This consists of '\0'-separated following elements: 90 | /// - long_about 91 | /// - after_long_help 92 | #[cfg(feature = "help")] 93 | cmd_doc: &'static str, 94 | 95 | /// Help string formatter. 96 | #[cfg(feature = "help")] 97 | fmt_help: FmtWriter, 98 | 99 | flattened: A, 100 | } 101 | 102 | impl RawArgsInfo { 103 | pub(crate) const EMPTY_REF: &'static Self = 104 | &RawArgsInfo::new(false, false, None, "", "", fmt_noop, []); 105 | 106 | // Used by proc-macro. 107 | pub const fn new( 108 | subcmd_optional: bool, 109 | mut has_optional_named: bool, 110 | subcmd_info: Option<&'static RawSubcommandInfo>, 111 | cmd_doc: &'static str, 112 | descriptions: &'static str, 113 | fmt_help: FmtWriter, 114 | flattened: [RawArgsInfoRef; N], 115 | ) -> RawArgsInfo<[RawArgsInfoRef; N]> { 116 | #[cfg(feature = "help")] 117 | { 118 | let len = flattened.len(); 119 | let mut i = 0usize; 120 | while i < len { 121 | let inner = flattened[i]; 122 | has_optional_named |= inner.0.has_optional_named; 123 | i += 1; 124 | } 125 | } 126 | 127 | RawArgsInfo { 128 | descriptions, 129 | 130 | #[cfg(feature = "help")] 131 | subcmd_optional, 132 | #[cfg(feature = "help")] 133 | has_optional_named, 134 | #[cfg(feature = "help")] 135 | subcmd_info, 136 | #[cfg(feature = "help")] 137 | cmd_doc, 138 | #[cfg(feature = "help")] 139 | fmt_help, 140 | 141 | flattened, 142 | } 143 | } 144 | 145 | // Used by proc-macro for construction of `RawSubcommandInfo`. 146 | pub const fn raw_cmd_docs(&self) -> &str { 147 | #[cfg(feature = "help")] 148 | { 149 | self.cmd_doc 150 | } 151 | #[cfg(not(feature = "help"))] 152 | { 153 | "" 154 | } 155 | } 156 | 157 | // FIXME: This is quite complex. Can we directly codegen byte offset of 158 | // description string, instead of using `idx`? 159 | pub(crate) fn get_description(&self, mut idx: u8) -> Option<&'static str> { 160 | // See `RawArgsInfo`. 161 | let mut desc = self.descriptions; 162 | while let Some((lhs, rhs)) = split_once(desc, b'\0') { 163 | if idx == 0 { 164 | return Some(lhs); 165 | } 166 | idx -= 1; 167 | desc = rhs; 168 | } 169 | for child in &self.flattened { 170 | if let Some(s) = child.0.get_description(idx) { 171 | return Some(s); 172 | } 173 | } 174 | None 175 | } 176 | 177 | #[cfg(feature = "help")] 178 | pub(crate) fn has_optional_named(&self) -> bool { 179 | self.has_optional_named 180 | } 181 | 182 | #[cfg(feature = "help")] 183 | pub(crate) fn fmt_help(&self, w: &mut dyn fmt::Write, what: u8) { 184 | (self.fmt_help)(w, what); 185 | for f in &self.flattened { 186 | f.0.fmt_help(w, what); 187 | } 188 | } 189 | 190 | #[cfg(feature = "help")] 191 | pub(crate) fn doc(&self) -> CommandDoc { 192 | let [long_about, after_long_help] = split_sep_many(self.cmd_doc, b'\0').unwrap_or([""; 2]); 193 | CommandDoc { long_about, after_long_help } 194 | } 195 | 196 | /// Iterate over subcommands and short descriptions. 197 | #[cfg(feature = "help")] 198 | pub(crate) fn subcommands( 199 | &self, 200 | ) -> Option + Clone> { 201 | let subcmd = self.subcmd_info?; 202 | Some(split_terminator(subcmd.subcommands, b'\0').zip( 203 | subcmd.cmd_docs.iter().map(|raw_doc| split_once(raw_doc, b'\0').unwrap_or(("", "")).0), 204 | )) 205 | } 206 | 207 | #[cfg(feature = "help")] 208 | pub(crate) fn subcommand_optional(&self) -> bool { 209 | self.subcmd_optional 210 | } 211 | } 212 | 213 | #[derive(Debug, Clone, Copy)] 214 | pub(crate) struct CommandDoc { 215 | pub(crate) long_about: &'static str, 216 | pub(crate) after_long_help: &'static str, 217 | } 218 | -------------------------------------------------------------------------------- /test-suite/src/bin/criterion-argh.rs: -------------------------------------------------------------------------------- 1 | //! See `./criterion-clap.rs`. 2 | #![expect(dead_code, reason = "fields are only for testing")] 3 | use std::str::FromStr; 4 | 5 | use argh::FromArgs; 6 | 7 | /// Criterion Benchmark 8 | #[derive(FromArgs)] 9 | struct Cli { 10 | /// skip benchmarks whose names do not contain FILTER. 11 | #[argh(positional)] 12 | filter: Vec, 13 | /// configure coloring of output. always = always colorize output, never = never colorize 14 | /// output, auto = colorize output if output is a tty and compiled for unix. 15 | #[argh(option, short = 'c', default = "Color::Auto")] 16 | color: Color, 17 | /// print additional statistical information. 18 | #[argh(switch, short = 'v')] 19 | verbose: bool, 20 | /// print only the benchmark results. 21 | #[argh(switch, short = 'q')] 22 | quiet: bool, 23 | /// disable plut and HTML generation. 24 | #[argh(switch, short = 'n')] 25 | noplot: bool, 26 | /// save results under a named baseline. 27 | #[argh(option, short = 's')] 28 | save_baseline: Option, 29 | /// discard benchmark results. 30 | #[argh(switch)] 31 | discard_baseline: bool, 32 | /// compare to a named baseline. If any benchmarks do not have the specified baseline this 33 | /// command fails. 34 | #[argh(option, short = 'b')] 35 | baseline: Option, 36 | /// baseline lenient 37 | #[argh(option)] 38 | baseline_lenient: Option, 39 | /// list all benchmarks 40 | #[argh(switch)] 41 | list: bool, 42 | /// output formatting 43 | #[argh(option, default = "Format::Pretty")] 44 | format: Format, 45 | /// list or run ignored benchmarks (currently means skip all benchmarks) 46 | #[argh(switch)] 47 | ignored: bool, 48 | /// run benchmarks that exactly match the provided filter 49 | #[argh(switch)] 50 | exact: bool, 51 | /// iterate each benchmark for approximately the given number of seconds, doing no analysis and 52 | /// without storing the results. Useful for running the benchmarks in a profiler. 53 | #[argh(option)] 54 | profile_time: Option, 55 | /// load a previous baseline instead of sampling new data. 56 | #[argh(option)] 57 | load_baseline: Option, 58 | /// changes the default size of the sample for this run. 59 | #[argh(option)] 60 | sample_size: Option, 61 | /// changes the default warm up time for this run. 62 | #[argh(option, default = "3.0")] 63 | warm_up_time: f64, 64 | /// changes the default measurement time for this run. 65 | #[argh(option, default = "5.0")] 66 | measurement_time: f64, 67 | /// changes the default number of resamples for this run. 68 | #[argh(option, default = "100000")] 69 | nresamples: usize, 70 | /// changes the default noise threshold for this run. 71 | #[argh(option, default = "0.01")] 72 | noise_threshld: f64, 73 | /// changes the default confidence level for this run. 74 | #[argh(option, default = "0.95")] 75 | confidence_level: f64, 76 | /// changes the default significance level for this run. 77 | #[argh(option, default = "0.05")] 78 | significance_level: f64, 79 | /// benchmark only until the significance level has been reached 80 | #[argh(switch)] 81 | quick: bool, 82 | /// run the benchmarks once, to verify that they execute successfully, but do not measure or 83 | /// report the results. 84 | #[argh(switch, hidden_help)] 85 | test: bool, 86 | #[argh(option, hidden_help)] 87 | bench: Option, 88 | /// set the plotting backend. By default, Criterion.rs will use the gnuplot backend if gnuplot 89 | /// is available, or the plotters backend if it isn't. 90 | #[argh(option, hidden_help)] 91 | plotting_backend: Option, 92 | /// change the CLI output format. By default, Criterion.rs will use its own format. If output 93 | /// format is set to 'bencher', Criterion.rs will print output in a format that resembles the 94 | /// 'bencher' crate. 95 | #[argh(option, hidden_help, default = "OutputFormat::Criterion")] 96 | output_format: OutputFormat, 97 | /// ignored, but added for compatibility with libtest. 98 | #[argh(switch, hidden_help)] 99 | nocapture: bool, 100 | /// ignored, but added for compatibility with libtest. 101 | #[argh(switch, hidden_help)] 102 | show_output: bool, 103 | /// ignored, but added for compatibility with libtest. 104 | #[argh(switch, hidden_help)] 105 | include_ignored: bool, 106 | #[argh(switch, short = 'V', hidden_help)] 107 | version: bool, 108 | } 109 | 110 | impl Cli { 111 | fn validate(&self) -> Result<(), &'static str> { 112 | if self.quiet && self.verbose { 113 | return Err("--quite conflicts with --verbose"); 114 | } 115 | if self.discard_baseline as u8 116 | + self.save_baseline.is_some() as u8 117 | + self.baseline.is_some() as u8 118 | + self.baseline_lenient.is_some() as u8 119 | > 1 120 | { 121 | return Err( 122 | "--discard-baseline, --save-baseline, --baseline nad --baseline-lenient conflicts with each other", 123 | ); 124 | } 125 | if self.list as u8 + self.test as u8 + self.profile_time.is_some() as u8 > 1 { 126 | return Err("--list, --tests and --profile-time conflicts with each other"); 127 | } 128 | if self.load_baseline.is_some() && self.profile_time.is_some() { 129 | return Err("--load-baseline conflicts with --profile-time"); 130 | } 131 | if self.load_baseline.is_some() && self.baseline.is_none() { 132 | return Err("--load-baseline requires --baseline"); 133 | } 134 | if self.quick && self.sample_size.is_some() { 135 | return Err("--quick conflicts with --sample-size"); 136 | } 137 | Ok(()) 138 | } 139 | } 140 | 141 | enum Color { 142 | Auto, 143 | Always, 144 | Never, 145 | } 146 | 147 | impl FromStr for Color { 148 | type Err = String; 149 | 150 | fn from_str(s: &str) -> Result { 151 | Ok(match s { 152 | "auto" => Self::Auto, 153 | "always" => Self::Always, 154 | "never" => Self::Never, 155 | _ => return Err(format!("invalid color: {s:?}")), 156 | }) 157 | } 158 | } 159 | 160 | enum Format { 161 | Pretty, 162 | Terse, 163 | } 164 | 165 | impl FromStr for Format { 166 | type Err = String; 167 | 168 | fn from_str(s: &str) -> Result { 169 | Ok(match s { 170 | "pretty" => Self::Pretty, 171 | "terse" => Self::Terse, 172 | _ => return Err(format!("invalid format: {s:?}")), 173 | }) 174 | } 175 | } 176 | 177 | enum PlottingBackend { 178 | Gnuplot, 179 | Plotters, 180 | } 181 | 182 | impl FromStr for PlottingBackend { 183 | type Err = String; 184 | 185 | fn from_str(s: &str) -> Result { 186 | Ok(match s { 187 | "gnuplut" => Self::Gnuplot, 188 | "plotters" => Self::Plotters, 189 | _ => return Err(format!("invalid plotting backend: {s:?}")), 190 | }) 191 | } 192 | } 193 | 194 | enum OutputFormat { 195 | Criterion, 196 | Test, 197 | } 198 | 199 | impl FromStr for OutputFormat { 200 | type Err = String; 201 | 202 | fn from_str(s: &str) -> Result { 203 | Ok(match s { 204 | "criterion" => Self::Criterion, 205 | "test" => Self::Test, 206 | _ => return Err(format!("invalid output format: {s:?}")), 207 | }) 208 | } 209 | } 210 | 211 | fn main() -> Result<(), String> { 212 | let cli = argh::from_env::(); 213 | cli.validate()?; 214 | std::hint::black_box(cli); 215 | Ok(()) 216 | } 217 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # palc 2 | 3 | [![crates.io](https://img.shields.io/crates/v/palc)](https://crates.io/crates/palc) 4 | [![docs.rs](https://img.shields.io/docsrs/palc)](https://docs.rs/palc) 5 | 6 | Prototype of a command line argument parser with several opposite design goals from [clap]. 7 | 8 | > ⚠️ This project is in alpha stage and is not ready for production yet. 9 | > The API is subject to change. Feedbacks are welcome. 10 | 11 | Check [`./test-suite/src/bin/*-palc.rs`](https://github.com/oxalica/palc/blob/main/test-suite/src/bin/criterion-palc.rs) for example usages. 12 | 13 | [clap]: https://github.com/clap-rs/clap 14 | 15 | ## Similar: Compatible[^1] Derive API 16 | 17 | palc is an un-opinionated[^2] derive-based argument parser. 18 | We choose to align with the clap 4.0 derive API: `Parser`, `Args` and `Subcommand` 19 | macros with almost compatible `command(..)` and `arg(..)` attributes. 20 | 21 | In most cases, switching between clap derive and palc derive is as easy as 22 | changing a line in `Cargo.toml` and relevant `use` statements. No vendor locking. 23 | Writing your CLI structs first before deciding which crate to use. 24 | 25 | ## Similar: Full-featured, out of the box 26 | 27 | palc also aim to provide a decent CLI experience under default features: 28 | help generations, non-UTF-8 support, argument constraints, `Args` composition, 29 | subcommands, you name it. 30 | 31 | Though some of clap features are not-yet-implemented. 32 | 33 |
34 | 35 | Yet Implemented features 36 | 37 | 38 | 39 | - Argument behaviors: 40 | - [x] Boolean flags `--verbose`. 41 | - [x] Named arguments `--long value`, `-svalue` 42 | - [x] Bundled short arguments `-czf` 43 | - [x] '='-separator `--long=v` `-f=v`. 44 | - [x] Aliases. 45 | - [x] Reject hyphen values. 46 | - [x] Allow hyphen values. 47 | - [ ] Space-delimited multi-values. 48 | - [x] Custom-delimited multi-values. 49 | - [ ] Multi-values with value-terminator. 50 | - [x] Unnamed/free/positional arguments `FILE`. 51 | - [x] Force no named arguments `--`. 52 | - [x] Greedy/tail arguments (`arg(trailing_var_arg)`). 53 | - [x] Last arguments after `--` (`arg(last)`). 54 | - [ ] Allow hyphen values. 55 | - [ ] Counting number of occurrence (`ArgAction::Count`). 56 | - [ ] Custom ArgAction. 57 | - [ ] Custom number of values (`arg(num_args)`). 58 | - [ ] Overrides. 59 | 60 | - List of [magic argument types](https://docs.rs/clap/4.5.40/clap/_derive/index.html#arg-types) with automatic default behaviors: 61 | - [x] `T where T: TryFrom<&OsStr> || TryFrom<&str> || FromStr` (named & unnamed) 62 | - [x] `bool` (named) 63 | - [x] `Option` (named) 64 | - [x] `Option>` (named) 65 | Optional argument with an optional value, eg. `--foo`, `--foo=bar` or nothing. 66 | It requires `require_equals = true` to avoid parsing ambiguity and confusion. 67 | See: 68 | - [x] `Vec` (named & unnamed) 69 | - [x] `Option>` (named & unnamed) 70 | - [ ] `Vec>` 71 | - [ ] `Option>>` 72 | 73 | - [x] Default values. (`arg(default_value_t)`) 74 | - [x] Default pre-parsed string value. (`arg(default_value)`) 75 | - Note: The provided string value will be parsed at runtime if the 76 | argument is missing. This will cause codegen degradation due to 77 | panic handling, and typos cannot be caught statically. 78 | Always use `arg(default_value_t)` if possible. 79 | - [ ] Default missing values. 80 | - [ ] Default from env. 81 | 82 | - Argument value parsing: 83 | - [x] `derive(ValueEnum)` 84 | - [x] `value(rename_all)` 85 | - [x] `value(name)` 86 | - [ ] `value(skip)` 87 | - [ ] `value(help)` 88 | - [x] Non-UTF-8 inputs `PathBuf`, `OsString`. 89 | - [x] Automatically picked custom parser via `From`, `From` or `FromStr`. 90 | - [x] `arg(ignore_case)` 91 | - Note: Only `ValueEnum` that has no UPPERCASE variants are supported yet, due to implementation limitation. 92 | 93 | - Argument validations: 94 | - [x] Reject duplicated arguments. 95 | - [x] Required. 96 | - [ ] Conditional required. 97 | - [x] Conflicts. 98 | - [x] Exclusive. 99 | - [ ] Args groups (one and only one argument). 100 | 101 | - Composition: 102 | - [x] `arg(flatten)`. 103 | - Note that non-flatten arguments always take precedence over flatten arguments. 104 | - [x] Flatten named arguments. 105 | - [ ] Flatten unnamed arguments. 106 | - [x] Subcommands. 107 | - [ ] Argv0 as subcommand (multi-call binary). 108 | - [x] Prefer parsing subcommand over unnamed arguments. 109 | - [x] Global args. 110 | - Note: Current implementation has limitations on the number of values it takes. 111 | And it only propagates up if the inner Args cannot accept the named arguments -- 112 | that is -- only one innermost Args on the ancestor chain will receive it, not all. 113 | 114 | - [x] Help generation. 115 | Note: Help text is only for human consumption. The precise format is unstable, 116 | may change at any time and is not expected to exactly follow `clap`'s help format 117 | (although that is our general direction). 118 | 119 | - [x] Long help `--help`. 120 | - [ ] Short help `-h`. 121 | - [ ] Version `--version`. 122 | - [x] Custom header and footer. 123 | - [ ] Hiding. 124 | - [ ] Possible values of enums. 125 | - [x] Default values via `arg(default_value{,_t})`. 126 | - [ ] Custom help subcommand or flags. 127 | 128 | - [ ] Helpful error messages. 129 | - [x] Error argument and reason. 130 | - [ ] Expected format. 131 | - [ ] Error suggestions ("did you mean"). 132 | - [ ] Custom help template. 133 | 134 | - Term features: 135 | - [ ] Colored output. 136 | - Wrap on terminal width. 137 | - We do not plan to implement this for now because its drawback outweighs 138 | its benefits. Word splitting and text rendering length with Unicode 139 | support is be very tricky and costly. It also hurts output reproducibility. 140 | 141 | - [ ] Reflection. 142 | - [ ] Completions. 143 | 144 |
145 | 146 | ## Different: Only via `derive` macros, statically 147 | 148 | The only way to define a CLI parser in palc is via `derive`-macros. It is not 149 | possible to manually write `impl` or even construct it dynamically. 150 | Argument parsers are prepared, validated and generated during compile time. 151 | The runtime does nothing other than parsing, thus has no startup overhead. 152 | Also no insta-panics at runtime! 153 | 154 | On the contrary, clap only works on builder API under the hood and its derive 155 | API translates attributes to its builder API. The parser is still composed, 156 | verified, and then executed at runtime. This suffers from 157 | [startup time penalty](https://github.com/clap-rs/clap/pull/4792). 158 | 159 | This implies we do more work in proc-macro while rustc does less work on 160 | generated code. In compilation time benchmarks, we outperform clap-derive in 161 | both full build and incremental build. 162 | 163 | ## Different: Binary size aware 164 | 165 | Despite how many features we have, we keep binary overhead in check. 166 | Our goal is to give a size overhead that *deserves* its features, without 167 | unreasonable or meaningless bloat. 168 | 169 | Unlike other min-size-centric projects, eg. [pico-args] or [gumdrop], we choose 170 | NOT to sacrifice CLI user experience, or force CLI designers to write more 171 | (repetitive) code. 172 | We are striving for a good balance between features and their cost. 173 | 174 | In the benchmarks ([`./bench.txt`](./bench.txt)), binary size of small-to-medium 175 | CLI structs using `palc` is comparable to and sometimes smaller than `argh`. 176 | 177 | [pico-args]: https://crates.io/crates/pico-args 178 | [gumdrop]: https://crates.io/crates/gumdrop 179 | 180 |
181 | 182 | #### Credit 183 | 184 | The derive interface is inspired and mimicking [`clap`][clap]'s derive interface. 185 | 186 | The palc runtime design is inspired by [`miniserde`](https://github.com/dtolnay/miniserde). 187 | 188 | #### License 189 | 190 | 191 | Licensed under either of
Apache License, Version 192 | 2.0 or MIT license at your option. 193 | 194 | 195 |
196 | 197 | 198 | Unless you explicitly state otherwise, any contribution intentionally submitted 199 | for inclusion in this crate by you, as defined in the Apache-2.0 license, shall 200 | be dual licensed as above, without any additional terms or conditions. 201 | 202 | 203 | [^1]: Due to design differences, some attributes cannot be implemented 204 | statically or require a different syntax. 205 | TODO: Document all attributes and notable differences with clap. 206 | 207 | [^2]: [argh] say they are "opinionated" as an excuse of subjective and "creative" choice on derive attribute names and letter case restrictions. We are against these. 208 | 209 | [argh]: https://github.com/google/argh 210 | -------------------------------------------------------------------------------- /derive/src/derive_subcommand.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use quote::{ToTokens, format_ident, quote, quote_spanned}; 3 | use syn::{Data, DataEnum, DeriveInput, Ident, Type}; 4 | 5 | use crate::common::{CommandMeta, wrap_anon_item}; 6 | use crate::derive_args::CommandDoc; 7 | use crate::error::catch_errors; 8 | use syn::spanned::Spanned; 9 | 10 | pub fn expand(input: &DeriveInput) -> TokenStream { 11 | assert_no_generics!(input); 12 | 13 | match catch_errors(|| match &input.data { 14 | Data::Enum(data) => Ok(wrap_anon_item(expand_for_enum(input, data))), 15 | Data::Struct(_) => abort!( 16 | Span::call_site(), 17 | "structs are only supported by `derive(Args)`, not by `derive(Subcommand)`", 18 | ), 19 | Data::Union(_) => abort!(Span::call_site(), "only enums are supported"), 20 | }) { 21 | Ok(out) => out, 22 | Err(mut tts) => { 23 | tts.extend(wrap_anon_item(fallback(&input.ident))); 24 | tts 25 | } 26 | } 27 | } 28 | 29 | fn fallback(ident: &Ident) -> TokenStream { 30 | quote! { 31 | #[automatically_derived] 32 | impl __rt::Subcommand for #ident {} 33 | } 34 | } 35 | 36 | struct SubcommandImpl<'i> { 37 | enum_name: &'i Ident, 38 | state_defs: TokenStream, 39 | variants: Vec>, 40 | } 41 | 42 | struct VariantImpl<'i> { 43 | arg_name: String, 44 | kind: VariantKind<'i>, 45 | } 46 | 47 | enum VariantKind<'i> { 48 | Unit { state_name: Ident }, 49 | Tuple { variant_name: &'i Ident, ty: &'i Type }, 50 | Struct { state_name: Ident }, 51 | } 52 | 53 | impl ToTokens for VariantKind<'_> { 54 | fn to_tokens(&self, tokens: &mut TokenStream) { 55 | tokens.extend(match &self { 56 | Self::Tuple { variant_name, ty } => quote_spanned! {ty.span()=> 57 | |__args, __cmd_name, __ancestors| { 58 | __rt::Ok(Self::#variant_name(__rt::try_parse_state::<<#ty as __rt::Args>::__State>( 59 | __args, 60 | __cmd_name, 61 | __ancestors, 62 | )?)) 63 | } 64 | }, 65 | Self::Unit { state_name, .. } | Self::Struct { state_name } => quote! { 66 | __rt::try_parse_state::<#state_name> 67 | }, 68 | }); 69 | } 70 | } 71 | 72 | fn expand_for_enum<'a>(def: &'a DeriveInput, data: &'a DataEnum) -> SubcommandImpl<'a> { 73 | let enum_name = &def.ident; 74 | let mut state_defs = TokenStream::new(); 75 | 76 | let variants = data 77 | .variants 78 | .iter() 79 | .filter_map(|variant| { 80 | let variant_name = &variant.ident; 81 | let arg_name = heck::AsKebabCase(variant_name.to_string()).to_string(); 82 | let cmd_meta = CommandMeta::parse_attrs_opt(&variant.attrs); 83 | 84 | let kind = match &variant.fields { 85 | syn::Fields::Unnamed(fields) => { 86 | if fields.unnamed.len() != 1 { 87 | emit_error!( 88 | variant_name, 89 | "subcommand tuple variant must have a single element", 90 | ); 91 | return None; 92 | } 93 | if cmd_meta.is_some() { 94 | emit_error!( 95 | variant_name, 96 | "`#[command(..)]` or doc-comments are ignored for newtype variants. \ 97 | Attributes on the inner type of this variant will be used instead." 98 | ); 99 | } 100 | VariantKind::Tuple { variant_name, ty: &fields.unnamed[0].ty } 101 | } 102 | // FIXME: Unfortunately we need to generate state for each unit variant, 103 | // because each has a distinct `RAW_ARGS_INFO`, and it will be 104 | // used for about-text display. 105 | // Maybe we can pass it via an argument rather than a dyn method? 106 | syn::Fields::Unit => { 107 | let state_name = 108 | format_ident!("{enum_name}{variant_name}State", span = variant_name.span()); 109 | 110 | UnitVariantStateImpl { 111 | state_name: &state_name, 112 | enum_name, 113 | variant_name, 114 | cmd_meta: cmd_meta.as_deref(), 115 | } 116 | .to_tokens(&mut state_defs); 117 | 118 | VariantKind::Unit { state_name } 119 | } 120 | syn::Fields::Named(fields) => { 121 | let state_name = 122 | format_ident!("{enum_name}{variant_name}State", span = variant_name.span()); 123 | 124 | let mut state = crate::derive_args::expand_state_def_impl( 125 | &def.vis, 126 | cmd_meta.as_deref(), 127 | state_name.clone(), 128 | enum_name.to_token_stream(), 129 | fields, 130 | ) 131 | .ok()?; 132 | state.output_ctor = Some(quote! { #enum_name :: #variant_name }); 133 | state.to_tokens(&mut state_defs); 134 | VariantKind::Struct { state_name } 135 | } 136 | }; 137 | 138 | Some(VariantImpl { arg_name, kind }) 139 | }) 140 | .collect::>(); 141 | 142 | SubcommandImpl { enum_name, state_defs, variants } 143 | } 144 | 145 | impl ToTokens for SubcommandImpl<'_> { 146 | fn to_tokens(&self, tokens: &mut TokenStream) { 147 | let Self { enum_name, state_defs, variants } = self; 148 | let arg_strs = variants.iter().map(|v| &v.arg_name); 149 | let cases = variants.iter().map(|v| &v.kind); 150 | 151 | let subcmds = arg_strs.clone().flat_map(|name| [name, "\0"]).collect::(); 152 | let cmd_docs = variants 153 | .iter() 154 | .map(|v| match &v.kind { 155 | VariantKind::Tuple { ty, .. } => quote! { 156 | <<#ty as __rt::Args>::__State as __rt::ParserState>::RAW_ARGS_INFO.raw_cmd_docs() 157 | }, 158 | VariantKind::Unit { state_name,.. } | 159 | VariantKind::Struct { state_name } => quote! { 160 | <#state_name as __rt::ParserState>::RAW_ARGS_INFO.raw_cmd_docs() 161 | } 162 | }) 163 | .collect::>(); 164 | 165 | tokens.extend(quote! { 166 | #[automatically_derived] 167 | impl __rt::Subcommand for #enum_name { 168 | const RAW_INFO: &'static __rt::RawSubcommandInfo = &__rt::RawSubcommandInfo::new( 169 | #subcmds, 170 | [#(#cmd_docs),*], 171 | ); 172 | 173 | // If there is no variant. 174 | #[allow(unreachable_code)] 175 | fn feed_subcommand(__name: &__rt::OsStr) -> __rt::FeedSubcommand { 176 | __rt::Some(match __name.to_str() { 177 | __rt::Some(__name) => match __name { 178 | #(#arg_strs => #cases,)* 179 | _ => return __rt::None, 180 | }, 181 | __rt::None => return __rt::None, 182 | }) 183 | } 184 | } 185 | 186 | #state_defs 187 | }); 188 | } 189 | } 190 | 191 | struct UnitVariantStateImpl<'a> { 192 | state_name: &'a Ident, 193 | enum_name: &'a Ident, 194 | variant_name: &'a Ident, 195 | cmd_meta: Option<&'a CommandMeta>, 196 | } 197 | 198 | impl ToTokens for UnitVariantStateImpl<'_> { 199 | fn to_tokens(&self, tokens: &mut TokenStream) { 200 | let Self { state_name, enum_name, variant_name, cmd_meta } = self; 201 | let cmd_doc = CommandDoc(*cmd_meta); 202 | 203 | tokens.extend(quote! { 204 | struct #state_name; 205 | 206 | impl ::std::default::Default for #state_name { 207 | fn default() -> Self { 208 | #state_name 209 | } 210 | } 211 | 212 | impl __rt::ParserState for #state_name { 213 | type Output = #enum_name; 214 | 215 | const RAW_ARGS_INFO: &'static __rt::RawArgsInfo = &__rt::RawArgsInfo::new( 216 | false, 217 | false, 218 | __rt::None, 219 | #cmd_doc, 220 | "", 221 | |_, _| {}, 222 | [], 223 | ); 224 | 225 | fn finish(&mut self) -> __rt::Result { 226 | __rt::Ok(#enum_name::#variant_name) 227 | } 228 | } 229 | 230 | impl __rt::ParserStateDyn for #state_name { 231 | fn info(&self) -> &'static __rt::RawArgsInfo { 232 | ::RAW_ARGS_INFO 233 | } 234 | } 235 | }); 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! *Prototype of a command line argument parser with several opposite design goals from [clap].* 2 | //! 3 | //! [clap]: https://github.com/clap-rs/clap 4 | //! 5 | //! > ⚠️ This project is in alpha stage and is not ready for production yet. 6 | //! > The API is subject to change. Feedbacks are welcome. 7 | //! 8 | //! See [repository README](https://github.com/oxalica/palc?tab=readme-ov-file#palc) for details. 9 | //! 10 | //! TODO: Documentations. 11 | #![forbid(unsafe_code)] 12 | use std::{ffi::OsString, path::Path}; 13 | 14 | use error::ErrorKind; 15 | use runtime::{ParserInternal, RawParser}; 16 | 17 | mod error; 18 | mod refl; 19 | mod runtime; 20 | mod shared; 21 | mod util; 22 | mod values; 23 | 24 | #[cfg(feature = "help")] 25 | mod help; 26 | 27 | pub use crate::error::Error; 28 | pub type Result = std::result::Result; 29 | 30 | /// Not public API. Only for proc-macro internal use. 31 | // To scan all usages: 32 | // ```sh 33 | // rg --only-matching --no-filename '\b__rt::\w+' | LC_COLLATE=C sort --unique 34 | // ``` 35 | #[doc(hidden)] 36 | pub mod __private { 37 | pub use std::convert::Infallible; 38 | pub use std::ffi::{OsStr, OsString}; 39 | pub use std::fmt; 40 | pub use std::marker::PhantomData; 41 | pub use std::num::NonZero; 42 | pub use std::ops::ControlFlow; 43 | pub use std::str::from_utf8; 44 | pub use {Default, Err, Fn, Iterator, None, Ok, Option, Some, Vec, bool, char, str, u8, usize}; 45 | 46 | pub use crate::runtime::*; 47 | 48 | // Macros. 49 | pub use crate::util::{const_concat_impl, const_concat_len}; 50 | pub use crate::{__const_concat, __gate_help}; 51 | pub use std::{assert, concat, env, format_args, unimplemented, unreachable}; 52 | 53 | pub use crate::values::{ 54 | InferValueParser, ValueEnum, ValueParser, assert_auto_infer_value_parser_ok, 55 | }; 56 | 57 | pub use crate::refl::{RawArgsInfo, RawArgsInfoRef, RawSubcommandInfo}; 58 | pub use crate::shared::ArgAttrs; 59 | pub use crate::{Parser, Result}; 60 | } 61 | 62 | /// Top-level command interface. 63 | /// 64 | /// You should only get an implementation via [`derive(Parser)`](macro@Parser). 65 | /// Do not manually implement this trait. 66 | pub trait Parser: ParserInternal + Sized + 'static { 67 | fn parse() -> Self { 68 | match Self::try_parse_from(std::env::args_os()) { 69 | Ok(v) => v, 70 | Err(err) => { 71 | eprintln!("{err}"); 72 | std::process::exit(1); 73 | } 74 | } 75 | } 76 | 77 | fn try_parse_from(iter: I) -> Result 78 | where 79 | I: IntoIterator, 80 | T: Into + Clone, 81 | { 82 | let mut iter = iter.into_iter().map(|s| s.into()); 83 | let arg0 = iter.next().ok_or(ErrorKind::MissingArg0)?; 84 | let program_name = Path::new(&arg0).file_name().unwrap_or(arg0.as_ref()); 85 | Self::__parse_toplevel(&mut RawParser::new(&mut iter), program_name) 86 | } 87 | 88 | #[cfg(feature = "help")] 89 | fn render_long_help(argv0: impl Into) -> String { 90 | Self::try_parse_from([argv0.into().into(), OsString::from("--help")]) 91 | .err() 92 | .unwrap() 93 | .try_into_help() 94 | .unwrap() 95 | } 96 | } 97 | 98 | /// Derive macro generating top-level [`Parser`][trait@Parser] implementation. 99 | /// 100 | /// This macro currently only accepts non-generic `struct`s, and is 101 | /// a superset of [`Args`][macro@Args]. 102 | /// `derive(Args)` must *NOT* be used if there is already `derive(Parser)`. 103 | /// 104 | /// All attributes supported on the struct are identical to [`Args`][macro@Args]. 105 | pub use palc_derive::Parser; 106 | 107 | /// Derive macro for a composable collection of CLI arguments. 108 | /// 109 | /// *Note: Currently only this derive-macro is part of public API, but the 110 | /// trait `Args` is not.* 111 | /// 112 | /// This macro only accepts non-generic `struct`s. The type deriving this macro 113 | /// can be used by other types via `#[command(flatten)]`. 114 | /// 115 | /// # Container attributes 116 | /// 117 | /// These subcommand-level attributes on `struct` are the description of the 118 | /// subcommand itself. They are only meaningful on top-level [`Parser`], or a 119 | /// struct used in [`Subcommand`] enum's tuple variant. If this struct is used 120 | /// as `#[command(flatten)]`, these container attributes are useless and ignored. 121 | /// 122 | /// - `#[command(name = "...")]` 123 | /// 124 | /// TODO: TBD 125 | /// 126 | /// - `#[command(version)]` or `#[command(version = EXPR)]` 127 | /// 128 | /// TODO: TBD 129 | /// 130 | /// - `#[command(long_about = CONST_EXPR)]` or `#[command(long_about)]` 131 | /// 132 | /// Override the default "about" text shown in the generated help output. 133 | /// 134 | /// `CONST_EXPR`, if present, must be valid in const-context and has type 135 | /// `&'static str`. 136 | /// 137 | /// If no explicit value (`= CONST_EXPR`) is set, package description from 138 | /// `Cargo.toml` is used as its value. 139 | /// If this attribute is not present, the doc-comment of the struct is used 140 | /// as its value. 141 | /// 142 | /// - `#[command(after_long_about = CONST_EXPR)]` 143 | /// 144 | /// `CONST_EXPR`, if present, must be valid in const-context and has type 145 | /// `&'static str`. 146 | /// 147 | /// Additional text to be printed after the generated help output. 148 | /// 149 | /// - `#[doc = "..."]` or `/// ...` 150 | /// 151 | /// The default value for `long_about` if that attribute is not present. 152 | /// 153 | /// # Field attributes for composition 154 | /// 155 | /// - `#[command(flatten)]` 156 | /// 157 | /// Flatten (inline) a collection of arguments into this struct. 158 | /// 159 | /// The field type must derive [`Args`], must not contains a subcommand field, 160 | /// and must not form a dependency cycle, otherwise a compile error is 161 | /// emitted. 162 | /// 163 | /// - Named arguments 164 | /// 165 | /// Since named arguments are unordered, flattened arguments and 166 | /// non-flattened ones can be parsed in any interleaving order. 167 | /// 168 | /// In a struct, all named arguments, including flattened ones, should *NOT* 169 | /// have collide names. Otherwise, the behavior is unspecified but 170 | /// deterministic, and may change in the future. 171 | /// The current behavior is: only one argument "wins" and consumes the value. 172 | /// 173 | /// - Unnamed arguments 174 | /// 175 | /// TODO: Not yet supported to be flattened. 176 | /// 177 | /// - `#[command(subcommand)]` 178 | /// 179 | /// Define a subcommand. At most one field can be marked as subcommand. 180 | /// The field type must be either `SubcmdType` or `Option` where 181 | /// `SubcmdType` derives [`Subcommand`]. 182 | /// 183 | /// # Field attributes for arguments 184 | /// 185 | /// TODO 186 | pub use palc_derive::Args; 187 | 188 | /// Derive macro for a composable enum of CLI subcommands. 189 | /// 190 | /// *Note: Currently only this derive-macro is part of public API, but the 191 | /// trait `Subcommand` is not.* 192 | /// 193 | /// This macro supports non-generic `enum`s. 194 | /// 195 | /// # Subcommand names 196 | /// 197 | /// Each variant corresponds to a subcommand with the name being the variant 198 | /// identifier converted to "kebab-case". 199 | /// Duplicated names after case conversion produce a compile error. 200 | /// 201 | /// # Supported variants 202 | /// 203 | /// - `UnitVariant` 204 | /// 205 | /// A subcommand with no arguments on its own. Additional 206 | /// arguments may still be accepted after it, if there is any from ancestor 207 | /// subcommands. 208 | /// 209 | /// [`#[command(..)]` container attributes][command_attrs] are allowed on this variant. 210 | /// 211 | /// - `StructVariant { .. }` 212 | /// 213 | /// An inlined subcommand. Variant fields are handled in 214 | /// the same way as fields of [`derive(Args)`][Args]. See its docs for details. 215 | /// 216 | /// [`#[command(..)]` container attributes][command_attrs] are allowed on this variant. 217 | /// 218 | /// - `TupleVariant(AnotherType)` 219 | /// 220 | /// An outlined subcommand where `AnotherType` must derive [`Args`]. 221 | /// 222 | /// [`#[command(..)]` container attributes][command_attrs] are *NOT* allowed on this variant. 223 | /// Instead, those on `AnotherType` are used. 224 | /// 225 | /// - Other kinds of variant are rejected. 226 | /// 227 | /// [command_attrs]: derive.Args.html#container-attributes 228 | pub use palc_derive::Subcommand; 229 | 230 | /// Derive macro for enums parsable from argument values. 231 | /// 232 | /// *Note: Currently only this derive-macro is part of public API, but the 233 | /// trait `ValueEnum` is not.* 234 | /// 235 | /// # Container attributes 236 | /// 237 | /// - `#[value(rename_all = "...")]` 238 | /// 239 | /// Override the variant name case conversion. 240 | /// When not present, the default value is `"kebab-case"`. 241 | /// 242 | /// All supported conversions: 243 | /// - `"camelCase"` 244 | /// - `"kebab-case"` 245 | /// - `"PascalCase"` 246 | /// - `"SCREAMING_SNAKE_CASE"` 247 | /// - `"snake_case"` 248 | /// - `"lower"` 249 | /// - `"UPPER"` 250 | /// - `"verbatim"` 251 | /// 252 | /// # Variant attributes 253 | /// 254 | /// - `#[value(name = "...")]` 255 | /// 256 | /// Override the default case-converted name for this variant. 257 | /// The string literal is taken verbatim for parsing, but it is still 258 | /// checked for forbiddgen characters and conflicts. See the next section. 259 | /// 260 | /// If not present, the variant name after case conversion (see previous section). 261 | /// 262 | /// # Errors 263 | /// 264 | /// - ASCII control characters are forbidden from subcommand names, due to 265 | /// implementation limitations. A compile error is emitted if any is found. 266 | /// 267 | /// - The macro will check all variant names for parsing (after case conversion if 268 | /// necessary) do not collide. Otherwise a compile error is emitted. 269 | pub use palc_derive::ValueEnum; 270 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr( 2 | not(feature = "default"), 3 | allow(dead_code, reason = "help generation code can be unused") 4 | )] 5 | use std::ffi::OsString; 6 | use std::fmt; 7 | 8 | use crate::runtime::{ParserChainNode, ParserState}; 9 | 10 | /// We use bound `UserErr: Into` for conversing user errors. 11 | /// This implies either `UserErr: std::error::Error` or it is string-like. 12 | /// 13 | /// Note that `&str: !std::error::Error` so we cannot just use `UserErr: std::error::Err`. 14 | pub(crate) type DynStdError = Box; 15 | 16 | pub struct Error(Box); 17 | 18 | #[cfg(test)] 19 | struct _AssertErrorIsSendSync 20 | where 21 | Error: Send + Sync; 22 | 23 | struct Inner { 24 | kind: ErrorKind, 25 | 26 | /// The target argument we are parsing into, when the error occurs. 27 | /// For unknown arguments or subcommand, this is `None`. 28 | arg_desc: Option<&'static str>, 29 | // The unexpected raw input we are parsing, when the error occurs. 30 | /// For finalization errors like constraint violation, this is `None`. 31 | input: Option, 32 | /// The underlying source error, if there is any. 33 | source: Option, 34 | /// Possible value strings, terminated by NUL. 35 | possible_inputs_nul: Option<&'static str>, 36 | 37 | /// Rendered help message. 38 | help: Option, 39 | } 40 | 41 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 42 | pub(crate) enum ErrorKind { 43 | // Input parsing errors. 44 | MissingArg0, 45 | InvalidUtf8, 46 | UnknownNamedArgument, 47 | UnknownSubcommand, 48 | DuplicatedNamedArgument, 49 | ExtraUnnamedArgument, 50 | UnexpectedInlineValue, 51 | MissingValue, 52 | InvalidValue, 53 | MissingEq, 54 | 55 | // Finalization errors. 56 | MissingRequiredArgument, 57 | MissingRequiredSubcommand, 58 | ConstraintRequired, 59 | ConstraintExclusive, 60 | ConstraintConflict, 61 | 62 | // Not really an error, but for bubbling out. 63 | Help, 64 | 65 | // User errors. 66 | Custom, 67 | } 68 | 69 | impl std::error::Error for Error { 70 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 71 | self.0.source.as_ref().map(|err| &**err as _) 72 | } 73 | } 74 | 75 | impl fmt::Debug for Error { 76 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 77 | let e = &*self.0; 78 | let mut s = f.debug_struct("Error"); 79 | s.field("kind", &e.kind) 80 | .field("arg_desc", &e.arg_desc) 81 | .field("input", &e.input) 82 | .field("source", &e.source) 83 | .field("help", &e.help); 84 | s.finish_non_exhaustive() 85 | } 86 | } 87 | 88 | impl fmt::Display for Error { 89 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 90 | let e = &*self.0; 91 | 92 | let opt_input = |f: &mut fmt::Formatter<'_>| { 93 | if let Some(input) = &e.input { 94 | f.write_str(" '")?; 95 | f.write_str(&input.to_string_lossy())?; 96 | f.write_str("'")?; 97 | } 98 | Ok(()) 99 | }; 100 | let opt_arg = |f: &mut fmt::Formatter<'_>, with_for: bool| { 101 | if let Some(desc) = &e.arg_desc { 102 | f.write_str(if with_for { " for '" } else { " '" })?; 103 | f.write_str(desc)?; 104 | f.write_str("'")?; 105 | } 106 | Ok(()) 107 | }; 108 | let opt_for_arg = |f: &mut fmt::Formatter<'_>| opt_arg(f, true); 109 | 110 | match &e.kind { 111 | ErrorKind::MissingArg0 => f.write_str("missing executable argument (argv[0])"), 112 | ErrorKind::InvalidUtf8 => { 113 | f.write_str("invalid UTF-8")?; 114 | opt_input(f)?; 115 | opt_for_arg(f) 116 | } 117 | ErrorKind::UnknownNamedArgument => { 118 | f.write_str("unexpected argument")?; 119 | opt_input(f) 120 | } 121 | ErrorKind::UnknownSubcommand => { 122 | f.write_str("unrecognized subcommand")?; 123 | opt_input(f) 124 | } 125 | ErrorKind::DuplicatedNamedArgument => { 126 | f.write_str("the argument")?; 127 | opt_arg(f, false)?; 128 | f.write_str(" cannot be used multiple times") 129 | } 130 | ErrorKind::ExtraUnnamedArgument => { 131 | f.write_str("unexpected argument")?; 132 | opt_input(f) 133 | } 134 | ErrorKind::UnexpectedInlineValue => { 135 | f.write_str("unexpected value")?; 136 | opt_input(f)?; 137 | opt_for_arg(f) 138 | } 139 | ErrorKind::MissingValue => { 140 | f.write_str("a value is required")?; 141 | opt_for_arg(f)?; 142 | f.write_str(" but none was supplied") 143 | } 144 | ErrorKind::InvalidValue => { 145 | f.write_str("invalid value")?; 146 | opt_input(f)?; 147 | opt_for_arg(f)?; 148 | if let Some(strs) = e.possible_inputs_nul { 149 | f.write_str("\n [possible values: ")?; 150 | let mut first = true; 151 | for s in strs.split_terminator('\0') { 152 | if first { 153 | first = false 154 | } else { 155 | f.write_str(", ")? 156 | } 157 | f.write_str(s)?; 158 | } 159 | f.write_str("]")?; 160 | } 161 | Ok(()) 162 | } 163 | ErrorKind::MissingEq => { 164 | f.write_str("equal sign is needed when assigning values")?; 165 | opt_for_arg(f) 166 | } 167 | 168 | ErrorKind::MissingRequiredArgument => { 169 | f.write_str("the argument")?; 170 | opt_arg(f, false)?; 171 | f.write_str(" is required but not provided") 172 | } 173 | ErrorKind::MissingRequiredSubcommand => { 174 | f.write_str("the subcommand is required but not provided") 175 | // TODO: Possible subcommands. 176 | } 177 | ErrorKind::ConstraintRequired => { 178 | f.write_str("the argument")?; 179 | opt_arg(f, false)?; 180 | f.write_str(" is required but not provided") 181 | } 182 | ErrorKind::ConstraintExclusive => { 183 | f.write_str("the argument")?; 184 | opt_arg(f, false)?; 185 | f.write_str(" cannot be used with one or more of the other specified arguments") 186 | } 187 | ErrorKind::ConstraintConflict => { 188 | f.write_str("the argument")?; 189 | opt_arg(f, false)?; 190 | // TODO: Conflict with what? 191 | f.write_str(" cannot be used with some other arguments") 192 | } 193 | 194 | ErrorKind::Help => f.write_str(e.help.as_deref().unwrap_or("help is not available")), 195 | 196 | ErrorKind::Custom => self.0.source.as_ref().unwrap().fmt(f), 197 | } 198 | } 199 | } 200 | 201 | impl Error { 202 | fn new(kind: ErrorKind) -> Self { 203 | Self(Box::new(Inner { 204 | kind, 205 | arg_desc: None, 206 | input: None, 207 | source: None, 208 | possible_inputs_nul: None, 209 | help: None, 210 | })) 211 | } 212 | 213 | /// Create an custom error with given reason. 214 | pub fn custom(reason: impl Into) -> Self { 215 | let source = reason.into().into(); 216 | let mut e = Self::new(ErrorKind::Custom); 217 | e.0.source = Some(source); 218 | e 219 | } 220 | 221 | /// Render the help string if this error indicates a `--help` is encounered. 222 | /// 223 | /// # Errors 224 | /// 225 | /// If this error is not about help or feature "help" is disabled, `Err(self)` is returned. 226 | pub fn try_into_help(mut self) -> Result { 227 | if let Some(help) = self.0.help.take() { Ok(help) } else { Err(self) } 228 | } 229 | 230 | pub(crate) fn with_source(mut self, source: DynStdError) -> Self { 231 | self.0.source = Some(source); 232 | self 233 | } 234 | 235 | pub(crate) fn with_arg_desc(mut self, arg_desc: Option<&'static str>) -> Self { 236 | self.0.arg_desc = arg_desc; 237 | self 238 | } 239 | 240 | pub(crate) fn with_possible_values(mut self, possible_inputs_nul: &'static str) -> Self { 241 | self.0.possible_inputs_nul = 242 | (!possible_inputs_nul.is_empty()).then_some(possible_inputs_nul); 243 | self 244 | } 245 | 246 | #[cfg(not(feature = "help"))] 247 | pub(crate) fn maybe_render_help(self, _chain: &mut ParserChainNode) -> Self { 248 | self 249 | } 250 | 251 | #[cfg(feature = "help")] 252 | pub(crate) fn maybe_render_help(mut self, chain: &mut ParserChainNode) -> Self { 253 | if self.0.kind == ErrorKind::Help { 254 | let out = self.0.help.insert(String::new()); 255 | crate::help::render_help_into(out, chain); 256 | } 257 | self 258 | } 259 | } 260 | 261 | impl From for Error { 262 | #[cold] 263 | fn from(kind: ErrorKind) -> Self { 264 | Self::new(kind) 265 | } 266 | } 267 | 268 | impl ErrorKind { 269 | #[cold] 270 | pub(crate) fn with_input(self, input: OsString) -> Error { 271 | let mut err = Error::new(self); 272 | err.0.input = Some(input); 273 | err 274 | } 275 | 276 | #[cold] 277 | pub(crate) fn with_arg_idx(self, arg_idx: u8) -> Error { 278 | let desc = S::RAW_ARGS_INFO.get_description(arg_idx); 279 | Error::new(self).with_arg_desc(desc) 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /tests/ui/args-flatten-non-args.stderr: -------------------------------------------------------------------------------- 1 | error[E0277]: cannot flatten `Deep` which is not a `palc::Args` 2 | --> tests/ui/args-flatten-non-args.rs:4:10 3 | | 4 | 4 | #[derive(palc::Args)] 5 | | ^^^^^^^^^^ this type is expected to have `derive(palc::Args)` but it is not 6 | | 7 | help: the trait `palc::__private::Args` is not implemented for `Deep` 8 | --> tests/ui/args-flatten-non-args.rs:10:1 9 | | 10 | 10 | struct Deep {} 11 | | ^^^^^^^^^^^ 12 | = help: the following other types implement trait `palc::__private::Args`: 13 | Cli1 14 | Cli2 15 | = note: this error originates in the derive macro `palc::Args` (in Nightly builds, run with -Z macro-backtrace for more info) 16 | 17 | error[E0277]: cannot flatten `Deep` which is not a `palc::Args` 18 | --> tests/ui/args-flatten-non-args.rs:4:10 19 | | 20 | 4 | #[derive(palc::Args)] 21 | | ^^^^^^^^^^ this type is expected to have `derive(palc::Args)` but it is not 22 | | 23 | help: within `Cli1State`, the trait `palc::__private::Args` is not implemented for `Deep` 24 | --> tests/ui/args-flatten-non-args.rs:10:1 25 | | 26 | 10 | struct Deep {} 27 | | ^^^^^^^^^^^ 28 | = help: the following other types implement trait `palc::__private::Args`: 29 | Cli1 30 | Cli2 31 | note: required because it appears within the type `Cli1State` 32 | --> tests/ui/args-flatten-non-args.rs:5:8 33 | | 34 | 5 | struct Cli1 { 35 | | ^^^^ 36 | note: required by a bound in `Default` 37 | --> $RUST/core/src/default.rs 38 | | 39 | | pub const trait Default: Sized { 40 | | ^^^^^ required by this bound in `Default` 41 | 42 | error[E0277]: cannot flatten `Deep` which is not a `palc::Args` 43 | --> tests/ui/args-flatten-non-args.rs:5:8 44 | | 45 | 5 | struct Cli1 { 46 | | ^^^^ this type is expected to have `derive(palc::Args)` but it is not 47 | | 48 | help: within `Cli1State`, the trait `palc::__private::Args` is not implemented for `Deep` 49 | --> tests/ui/args-flatten-non-args.rs:10:1 50 | | 51 | 10 | struct Deep {} 52 | | ^^^^^^^^^^^ 53 | = help: the following other types implement trait `palc::__private::Args`: 54 | Cli1 55 | Cli2 56 | note: required because it appears within the type `Cli1State` 57 | --> tests/ui/args-flatten-non-args.rs:5:8 58 | | 59 | 5 | struct Cli1 { 60 | | ^^^^ 61 | = note: required for `Cli1State` to implement `Default` 62 | note: required by a bound in `ParserState` 63 | --> src/runtime.rs 64 | | 65 | | pub trait ParserState: Default + ParserStateDyn { 66 | | ^^^^^^^ required by this bound in `ParserState` 67 | 68 | error[E0277]: cannot flatten `Deep` which is not a `palc::Args` 69 | --> tests/ui/args-flatten-non-args.rs:5:8 70 | | 71 | 5 | struct Cli1 { 72 | | ^^^^ this type is expected to have `derive(palc::Args)` but it is not 73 | | 74 | help: within `Cli1State`, the trait `palc::__private::Args` is not implemented for `Deep` 75 | --> tests/ui/args-flatten-non-args.rs:10:1 76 | | 77 | 10 | struct Deep {} 78 | | ^^^^^^^^^^^ 79 | = help: the following other types implement trait `palc::__private::Args`: 80 | Cli1 81 | Cli2 82 | note: required because it appears within the type `Cli1State` 83 | --> tests/ui/args-flatten-non-args.rs:5:8 84 | | 85 | 5 | struct Cli1 { 86 | | ^^^^ 87 | note: required by a bound in `palc::__private::Args::__State` 88 | --> src/runtime.rs 89 | | 90 | | type __State: ParserState; 91 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `Args::__State` 92 | 93 | error[E0277]: cannot flatten `Subcmd` which is not a `palc::Args` 94 | --> tests/ui/args-flatten-non-args.rs:12:10 95 | | 96 | 12 | #[derive(palc::Args)] 97 | | ^^^^^^^^^^ this type is expected to have `derive(palc::Args)` but it is not 98 | | 99 | help: the trait `palc::__private::Args` is not implemented for `Subcmd` 100 | --> tests/ui/args-flatten-non-args.rs:19:1 101 | | 102 | 19 | enum Subcmd {} 103 | | ^^^^^^^^^^^ 104 | = help: the following other types implement trait `palc::__private::Args`: 105 | Cli1 106 | Cli2 107 | = note: this error originates in the derive macro `palc::Args` (in Nightly builds, run with -Z macro-backtrace for more info) 108 | 109 | error[E0277]: cannot flatten `Subcmd` which is not a `palc::Args` 110 | --> tests/ui/args-flatten-non-args.rs:12:10 111 | | 112 | 12 | #[derive(palc::Args)] 113 | | ^^^^^^^^^^ this type is expected to have `derive(palc::Args)` but it is not 114 | | 115 | help: within `Cli2State`, the trait `palc::__private::Args` is not implemented for `Subcmd` 116 | --> tests/ui/args-flatten-non-args.rs:19:1 117 | | 118 | 19 | enum Subcmd {} 119 | | ^^^^^^^^^^^ 120 | = help: the following other types implement trait `palc::__private::Args`: 121 | Cli1 122 | Cli2 123 | note: required because it appears within the type `Cli2State` 124 | --> tests/ui/args-flatten-non-args.rs:13:8 125 | | 126 | 13 | struct Cli2 { 127 | | ^^^^ 128 | note: required by a bound in `Default` 129 | --> $RUST/core/src/default.rs 130 | | 131 | | pub const trait Default: Sized { 132 | | ^^^^^ required by this bound in `Default` 133 | 134 | error[E0277]: cannot flatten `Subcmd` which is not a `palc::Args` 135 | --> tests/ui/args-flatten-non-args.rs:13:8 136 | | 137 | 13 | struct Cli2 { 138 | | ^^^^ this type is expected to have `derive(palc::Args)` but it is not 139 | | 140 | help: within `Cli2State`, the trait `palc::__private::Args` is not implemented for `Subcmd` 141 | --> tests/ui/args-flatten-non-args.rs:19:1 142 | | 143 | 19 | enum Subcmd {} 144 | | ^^^^^^^^^^^ 145 | = help: the following other types implement trait `palc::__private::Args`: 146 | Cli1 147 | Cli2 148 | note: required because it appears within the type `Cli2State` 149 | --> tests/ui/args-flatten-non-args.rs:13:8 150 | | 151 | 13 | struct Cli2 { 152 | | ^^^^ 153 | = note: required for `Cli2State` to implement `Default` 154 | note: required by a bound in `ParserState` 155 | --> src/runtime.rs 156 | | 157 | | pub trait ParserState: Default + ParserStateDyn { 158 | | ^^^^^^^ required by this bound in `ParserState` 159 | 160 | error[E0277]: cannot flatten `Subcmd` which is not a `palc::Args` 161 | --> tests/ui/args-flatten-non-args.rs:13:8 162 | | 163 | 13 | struct Cli2 { 164 | | ^^^^ this type is expected to have `derive(palc::Args)` but it is not 165 | | 166 | help: within `Cli2State`, the trait `palc::__private::Args` is not implemented for `Subcmd` 167 | --> tests/ui/args-flatten-non-args.rs:19:1 168 | | 169 | 19 | enum Subcmd {} 170 | | ^^^^^^^^^^^ 171 | = help: the following other types implement trait `palc::__private::Args`: 172 | Cli1 173 | Cli2 174 | note: required because it appears within the type `Cli2State` 175 | --> tests/ui/args-flatten-non-args.rs:13:8 176 | | 177 | 13 | struct Cli2 { 178 | | ^^^^ 179 | note: required by a bound in `palc::__private::Args::__State` 180 | --> src/runtime.rs 181 | | 182 | | type __State: ParserState; 183 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `Args::__State` 184 | 185 | error[E0277]: cannot flatten `Deep` which is not a `palc::Args` 186 | --> tests/ui/args-flatten-non-args.rs:4:10 187 | | 188 | 4 | #[derive(palc::Args)] 189 | | ^^^^^^^^^^ this type is expected to have `derive(palc::Args)` but it is not 190 | | 191 | help: within `Cli1State`, the trait `palc::__private::Args` is not implemented for `Deep` 192 | --> tests/ui/args-flatten-non-args.rs:10:1 193 | | 194 | 10 | struct Deep {} 195 | | ^^^^^^^^^^^ 196 | = help: the following other types implement trait `palc::__private::Args`: 197 | Cli1 198 | Cli2 199 | note: required because it appears within the type `Cli1State` 200 | --> tests/ui/args-flatten-non-args.rs:5:8 201 | | 202 | 5 | struct Cli1 { 203 | | ^^^^ 204 | = note: the return type of a function must have a statically known size 205 | 206 | error[E0277]: cannot flatten `Subcmd` which is not a `palc::Args` 207 | --> tests/ui/args-flatten-non-args.rs:12:10 208 | | 209 | 12 | #[derive(palc::Args)] 210 | | ^^^^^^^^^^ this type is expected to have `derive(palc::Args)` but it is not 211 | | 212 | help: within `Cli2State`, the trait `palc::__private::Args` is not implemented for `Subcmd` 213 | --> tests/ui/args-flatten-non-args.rs:19:1 214 | | 215 | 19 | enum Subcmd {} 216 | | ^^^^^^^^^^^ 217 | = help: the following other types implement trait `palc::__private::Args`: 218 | Cli1 219 | Cli2 220 | note: required because it appears within the type `Cli2State` 221 | --> tests/ui/args-flatten-non-args.rs:13:8 222 | | 223 | 13 | struct Cli2 { 224 | | ^^^^ 225 | = note: the return type of a function must have a statically known size 226 | 227 | error[E0277]: cannot flatten `Deep` which is not a `palc::Args` 228 | --> tests/ui/args-flatten-non-args.rs:7:11 229 | | 230 | 7 | deep: Deep, 231 | | ^^^^ this type is expected to have `derive(palc::Args)` but it is not 232 | | 233 | help: the trait `palc::__private::Args` is not implemented for `Deep` 234 | --> tests/ui/args-flatten-non-args.rs:10:1 235 | | 236 | 10 | struct Deep {} 237 | | ^^^^^^^^^^^ 238 | = help: the following other types implement trait `palc::__private::Args`: 239 | Cli1 240 | Cli2 241 | 242 | error[E0277]: cannot flatten `Deep` which is not a `palc::Args` 243 | --> tests/ui/args-flatten-non-args.rs:4:10 244 | | 245 | 4 | #[derive(palc::Args)] 246 | | ^^^^^^^^^^ this type is expected to have `derive(palc::Args)` but it is not 247 | | 248 | help: the trait `palc::__private::Args` is not implemented for `Deep` 249 | --> tests/ui/args-flatten-non-args.rs:10:1 250 | | 251 | 10 | struct Deep {} 252 | | ^^^^^^^^^^^ 253 | = help: the following other types implement trait `palc::__private::Args`: 254 | Cli1 255 | Cli2 256 | 257 | error[E0277]: cannot flatten `Subcmd` which is not a `palc::Args` 258 | --> tests/ui/args-flatten-non-args.rs:15:11 259 | | 260 | 15 | deep: Subcmd, 261 | | ^^^^^^ this type is expected to have `derive(palc::Args)` but it is not 262 | | 263 | help: the trait `palc::__private::Args` is not implemented for `Subcmd` 264 | --> tests/ui/args-flatten-non-args.rs:19:1 265 | | 266 | 19 | enum Subcmd {} 267 | | ^^^^^^^^^^^ 268 | = help: the following other types implement trait `palc::__private::Args`: 269 | Cli1 270 | Cli2 271 | 272 | error[E0277]: cannot flatten `Subcmd` which is not a `palc::Args` 273 | --> tests/ui/args-flatten-non-args.rs:12:10 274 | | 275 | 12 | #[derive(palc::Args)] 276 | | ^^^^^^^^^^ this type is expected to have `derive(palc::Args)` but it is not 277 | | 278 | help: the trait `palc::__private::Args` is not implemented for `Subcmd` 279 | --> tests/ui/args-flatten-non-args.rs:19:1 280 | | 281 | 19 | enum Subcmd {} 282 | | ^^^^^^^^^^^ 283 | = help: the following other types implement trait `palc::__private::Args`: 284 | Cli1 285 | Cli2 286 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "anstyle" 7 | version = "1.0.13" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" 10 | 11 | [[package]] 12 | name = "argh" 13 | version = "0.1.13" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "34ff18325c8a36b82f992e533ece1ec9f9a9db446bd1c14d4f936bac88fcd240" 16 | dependencies = [ 17 | "argh_derive", 18 | "argh_shared", 19 | "rust-fuzzy-search", 20 | ] 21 | 22 | [[package]] 23 | name = "argh_derive" 24 | version = "0.1.13" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | checksum = "adb7b2b83a50d329d5d8ccc620f5c7064028828538bdf5646acd60dc1f767803" 27 | dependencies = [ 28 | "argh_shared", 29 | "proc-macro2", 30 | "quote", 31 | "syn", 32 | ] 33 | 34 | [[package]] 35 | name = "argh_shared" 36 | version = "0.1.13" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "a464143cc82dedcdc3928737445362466b7674b5db4e2eb8e869846d6d84f4f6" 39 | dependencies = [ 40 | "serde", 41 | ] 42 | 43 | [[package]] 44 | name = "clap" 45 | version = "4.5.52" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "aa8120877db0e5c011242f96806ce3c94e0737ab8108532a76a3300a01db2ab8" 48 | dependencies = [ 49 | "clap_builder", 50 | "clap_derive", 51 | ] 52 | 53 | [[package]] 54 | name = "clap_builder" 55 | version = "4.5.52" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "02576b399397b659c26064fbc92a75fede9d18ffd5f80ca1cd74ddab167016e1" 58 | dependencies = [ 59 | "anstyle", 60 | "clap_lex", 61 | ] 62 | 63 | [[package]] 64 | name = "clap_derive" 65 | version = "4.5.49" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" 68 | dependencies = [ 69 | "heck", 70 | "proc-macro2", 71 | "quote", 72 | "syn", 73 | ] 74 | 75 | [[package]] 76 | name = "clap_lex" 77 | version = "0.7.6" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" 80 | 81 | [[package]] 82 | name = "dissimilar" 83 | version = "1.0.10" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "8975ffdaa0ef3661bfe02dbdcc06c9f829dfafe6a3c474de366a8d5e44276921" 86 | 87 | [[package]] 88 | name = "equivalent" 89 | version = "1.0.2" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 92 | 93 | [[package]] 94 | name = "expect-test" 95 | version = "1.5.1" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "63af43ff4431e848fb47472a920f14fa71c24de13255a5692e93d4e90302acb0" 98 | dependencies = [ 99 | "dissimilar", 100 | "once_cell", 101 | ] 102 | 103 | [[package]] 104 | name = "glob" 105 | version = "0.3.3" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" 108 | 109 | [[package]] 110 | name = "hashbrown" 111 | version = "0.16.0" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" 114 | 115 | [[package]] 116 | name = "heck" 117 | version = "0.5.0" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 120 | 121 | [[package]] 122 | name = "indexmap" 123 | version = "2.12.0" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" 126 | dependencies = [ 127 | "equivalent", 128 | "hashbrown", 129 | ] 130 | 131 | [[package]] 132 | name = "itoa" 133 | version = "1.0.15" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 136 | 137 | [[package]] 138 | name = "memchr" 139 | version = "2.7.6" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" 142 | 143 | [[package]] 144 | name = "once_cell" 145 | version = "1.21.3" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 148 | 149 | [[package]] 150 | name = "os_str_bytes" 151 | version = "7.1.1" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "63eceb7b5d757011a87d08eb2123db15d87fb0c281f65d101ce30a1e96c3ad5c" 154 | 155 | [[package]] 156 | name = "palc" 157 | version = "0.0.2" 158 | dependencies = [ 159 | "argh", 160 | "clap", 161 | "expect-test", 162 | "os_str_bytes", 163 | "palc-derive", 164 | "ref-cast", 165 | "trybuild", 166 | ] 167 | 168 | [[package]] 169 | name = "palc-derive" 170 | version = "0.0.2" 171 | dependencies = [ 172 | "heck", 173 | "prettyplease", 174 | "proc-macro2", 175 | "quote", 176 | "syn", 177 | ] 178 | 179 | [[package]] 180 | name = "prettyplease" 181 | version = "0.2.37" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" 184 | dependencies = [ 185 | "proc-macro2", 186 | "syn", 187 | ] 188 | 189 | [[package]] 190 | name = "proc-macro2" 191 | version = "1.0.103" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" 194 | dependencies = [ 195 | "unicode-ident", 196 | ] 197 | 198 | [[package]] 199 | name = "quote" 200 | version = "1.0.42" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" 203 | dependencies = [ 204 | "proc-macro2", 205 | ] 206 | 207 | [[package]] 208 | name = "ref-cast" 209 | version = "1.0.25" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" 212 | dependencies = [ 213 | "ref-cast-impl", 214 | ] 215 | 216 | [[package]] 217 | name = "ref-cast-impl" 218 | version = "1.0.25" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" 221 | dependencies = [ 222 | "proc-macro2", 223 | "quote", 224 | "syn", 225 | ] 226 | 227 | [[package]] 228 | name = "rust-fuzzy-search" 229 | version = "0.1.1" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "a157657054ffe556d8858504af8a672a054a6e0bd9e8ee531059100c0fa11bb2" 232 | 233 | [[package]] 234 | name = "ryu" 235 | version = "1.0.20" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 238 | 239 | [[package]] 240 | name = "serde" 241 | version = "1.0.228" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 244 | dependencies = [ 245 | "serde_core", 246 | "serde_derive", 247 | ] 248 | 249 | [[package]] 250 | name = "serde_core" 251 | version = "1.0.228" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 254 | dependencies = [ 255 | "serde_derive", 256 | ] 257 | 258 | [[package]] 259 | name = "serde_derive" 260 | version = "1.0.228" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 263 | dependencies = [ 264 | "proc-macro2", 265 | "quote", 266 | "syn", 267 | ] 268 | 269 | [[package]] 270 | name = "serde_json" 271 | version = "1.0.145" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" 274 | dependencies = [ 275 | "itoa", 276 | "memchr", 277 | "ryu", 278 | "serde", 279 | "serde_core", 280 | ] 281 | 282 | [[package]] 283 | name = "serde_spanned" 284 | version = "1.0.3" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" 287 | dependencies = [ 288 | "serde_core", 289 | ] 290 | 291 | [[package]] 292 | name = "syn" 293 | version = "2.0.110" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" 296 | dependencies = [ 297 | "proc-macro2", 298 | "quote", 299 | "unicode-ident", 300 | ] 301 | 302 | [[package]] 303 | name = "target-triple" 304 | version = "1.0.0" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b" 307 | 308 | [[package]] 309 | name = "termcolor" 310 | version = "1.4.1" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 313 | dependencies = [ 314 | "winapi-util", 315 | ] 316 | 317 | [[package]] 318 | name = "test-suite" 319 | version = "0.1.0" 320 | dependencies = [ 321 | "argh", 322 | "clap", 323 | "palc", 324 | ] 325 | 326 | [[package]] 327 | name = "toml" 328 | version = "0.9.8" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" 331 | dependencies = [ 332 | "indexmap", 333 | "serde_core", 334 | "serde_spanned", 335 | "toml_datetime", 336 | "toml_parser", 337 | "toml_writer", 338 | "winnow", 339 | ] 340 | 341 | [[package]] 342 | name = "toml_datetime" 343 | version = "0.7.3" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" 346 | dependencies = [ 347 | "serde_core", 348 | ] 349 | 350 | [[package]] 351 | name = "toml_parser" 352 | version = "1.0.4" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" 355 | dependencies = [ 356 | "winnow", 357 | ] 358 | 359 | [[package]] 360 | name = "toml_writer" 361 | version = "1.0.4" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" 364 | 365 | [[package]] 366 | name = "trybuild" 367 | version = "1.0.114" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "3e17e807bff86d2a06b52bca4276746584a78375055b6e45843925ce2802b335" 370 | dependencies = [ 371 | "dissimilar", 372 | "glob", 373 | "serde", 374 | "serde_derive", 375 | "serde_json", 376 | "target-triple", 377 | "termcolor", 378 | "toml", 379 | ] 380 | 381 | [[package]] 382 | name = "unicode-ident" 383 | version = "1.0.22" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" 386 | 387 | [[package]] 388 | name = "winapi-util" 389 | version = "0.1.11" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" 392 | dependencies = [ 393 | "windows-sys", 394 | ] 395 | 396 | [[package]] 397 | name = "windows-link" 398 | version = "0.2.1" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 401 | 402 | [[package]] 403 | name = "windows-sys" 404 | version = "0.61.2" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" 407 | dependencies = [ 408 | "windows-link", 409 | ] 410 | 411 | [[package]] 412 | name = "winnow" 413 | version = "0.7.13" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" 416 | -------------------------------------------------------------------------------- /tests/parse.rs: -------------------------------------------------------------------------------- 1 | use expect_test::{Expect, expect}; 2 | use palc::{Args, Parser, Subcommand}; 3 | use std::{ffi::OsString, fmt::Debug}; 4 | 5 | #[derive(Debug, Parser)] 6 | struct CliEmpty {} 7 | 8 | #[track_caller] 9 | fn check( 10 | args: impl IntoIterator + Clone>, 11 | expect: &P, 12 | ) { 13 | let got = P::try_parse_from(args).unwrap(); 14 | assert_eq!(got, *expect); 15 | } 16 | 17 | #[track_caller] 18 | fn check_err( 19 | args: impl IntoIterator + Clone>, 20 | expect: Expect, 21 | ) { 22 | let ret = P::try_parse_from(args).unwrap_err(); 23 | expect.assert_eq(&ret.to_string()); 24 | } 25 | 26 | #[derive(Debug, Clone, PartialEq, Subcommand)] 27 | enum Sub { 28 | Sub, 29 | } 30 | 31 | #[test] 32 | fn argv0() { 33 | check_err::(None::<&str>, expect!["missing executable argument (argv[0])"]); 34 | } 35 | 36 | #[test] 37 | fn short() { 38 | #[derive(Debug, PartialEq, Parser)] 39 | struct Cli { 40 | #[arg(short)] 41 | verbose: bool, 42 | #[arg(short)] 43 | debug: bool, 44 | #[arg(short)] 45 | file: Option, 46 | } 47 | 48 | check(["", "-dvf-"], &Cli { verbose: true, debug: true, file: Some("-".into()) }); 49 | check(["", "-f=-", "-v"], &Cli { verbose: true, debug: false, file: Some("-".into()) }); 50 | check(["", "-d", "-f", "-", "-v"], &Cli { verbose: true, debug: true, file: Some("-".into()) }); 51 | } 52 | 53 | #[test] 54 | fn flag() { 55 | #[derive(Debug, PartialEq, Parser)] 56 | struct Cli { 57 | #[arg(short, long)] 58 | verbose: bool, 59 | } 60 | 61 | check(["", "--verbose"], &Cli { verbose: true }); 62 | check(["", "-v"], &Cli { verbose: true }); 63 | 64 | check_err::( 65 | ["", "-vv"], 66 | expect!["the argument '-v, --verbose' cannot be used multiple times"], 67 | ); 68 | check_err::( 69 | ["", "--verbose", "-v"], 70 | expect!["the argument '-v, --verbose' cannot be used multiple times"], 71 | ); 72 | 73 | check_err::(["", "--verbose=9"], expect!["unexpected value '9' for '-v, --verbose'"]); 74 | check_err::(["", "-vd"], expect!["unexpected argument '-d'"]); 75 | check_err::(["", "-v="], expect!["unexpected value '' for '-v, --verbose'"]); 76 | } 77 | 78 | #[test] 79 | fn require_equals() { 80 | #[derive(Debug, PartialEq, Parser)] 81 | struct Cli { 82 | #[arg(long, short, require_equals = true)] 83 | file: Option, 84 | #[arg(short)] 85 | verbose: bool, 86 | } 87 | 88 | check_err::( 89 | ["", "--file", "-"], 90 | expect!["equal sign is needed when assigning values for '-f, --file='"], 91 | ); 92 | check_err::( 93 | ["", "-f", "-"], 94 | expect!["equal sign is needed when assigning values for '-f, --file='"], 95 | ); 96 | check_err::( 97 | ["", "-vf", "-"], 98 | expect!["equal sign is needed when assigning values for '-f, --file='"], 99 | ); 100 | check_err::( 101 | ["", "-fv"], 102 | expect!["equal sign is needed when assigning values for '-f, --file='"], 103 | ); 104 | 105 | check(["", "-f="], &Cli { verbose: false, file: Some("".into()) }); 106 | check(["", "-f=v"], &Cli { verbose: false, file: Some("v".into()) }); 107 | check(["", "-vf=v"], &Cli { verbose: true, file: Some("v".into()) }); 108 | check(["", "--file="], &Cli { verbose: false, file: Some("".into()) }); 109 | check(["", "--file=v"], &Cli { verbose: false, file: Some("v".into()) }); 110 | } 111 | 112 | #[test] 113 | fn required() { 114 | #[derive(Debug, PartialEq, Parser)] 115 | struct Cli { 116 | // TODO: Reject bool as unnamed arguments. It is almost always an typo. 117 | #[arg(long)] 118 | key: String, 119 | file: String, 120 | #[command(subcommand)] 121 | sub: Sub, 122 | } 123 | 124 | check_err::( 125 | ["", "path"], 126 | expect!["the argument '--key ' is required but not provided"], 127 | ); 128 | check_err::( 129 | ["", "--key", "value"], 130 | expect!["the argument '' is required but not provided"], 131 | ); 132 | check_err::( 133 | ["", "--key", "value", "path"], 134 | expect!["the subcommand is required but not provided"], 135 | ); 136 | check_err::( 137 | ["", "path", "sub"], 138 | expect!["the argument '--key ' is required but not provided"], 139 | ); 140 | 141 | check_err::( 142 | ["", "--key", "value", "sub"], 143 | expect!["the argument '' is required but not provided"], 144 | ); 145 | 146 | check_err::( 147 | ["", "--key", "value", "--key=value"], 148 | expect!["the argument '--key ' cannot be used multiple times"], 149 | ); 150 | 151 | let expect = Cli { key: "value".into(), file: "path".into(), sub: Sub::Sub }; 152 | check(["", "path", "--key", "value", "sub"], &expect); 153 | check(["", "--key", "value", "path", "sub"], &expect); 154 | } 155 | 156 | #[test] 157 | fn optional() { 158 | #[derive(Debug, Clone, Default, PartialEq, Parser)] 159 | struct Cli { 160 | #[arg(long)] 161 | flag: bool, 162 | #[arg(long)] 163 | key: Option, 164 | file: Option, 165 | #[command(subcommand)] 166 | sub: Option, 167 | } 168 | 169 | let default = Cli::default(); 170 | check([""], &default); 171 | check(["", "--flag"], &Cli { flag: true, ..default.clone() }); 172 | check(["", "--key", "value"], &Cli { key: Some("value".into()), ..default.clone() }); 173 | check(["", "path"], &Cli { file: Some("path".into()), ..default.clone() }); 174 | 175 | check(["", "sub"], &Cli { sub: Some(Sub::Sub), ..default.clone() }); 176 | 177 | check( 178 | ["", "--key", "sub", "path"], 179 | &Cli { key: Some("sub".into()), file: Some("path".into()), ..default.clone() }, 180 | ); 181 | check( 182 | ["", "path", "sub"], 183 | &Cli { file: Some("path".into()), sub: Some(Sub::Sub), ..default.clone() }, 184 | ); 185 | 186 | check( 187 | ["", "--flag", "--key", "value", "path", "sub"], 188 | &Cli { 189 | flag: true, 190 | key: Some("value".into()), 191 | file: Some("path".into()), 192 | sub: Some(Sub::Sub), 193 | }, 194 | ); 195 | 196 | check_err::( 197 | ["", "--key", "value", "--key=value"], 198 | expect!["the argument '--key ' cannot be used multiple times"], 199 | ); 200 | check_err::( 201 | ["", "--flag", "--flag"], 202 | expect!["the argument '--flag' cannot be used multiple times"], 203 | ); 204 | } 205 | 206 | #[test] 207 | fn option_option() { 208 | #[derive(Debug, Clone, Default, PartialEq, Parser)] 209 | struct Cli { 210 | #[arg(long)] 211 | foo: Option, 212 | #[arg(long, require_equals = true)] 213 | bar: Option>, 214 | } 215 | 216 | check([""], &Cli { foo: None, bar: None }); 217 | check(["", "--foo=", "--bar="], &Cli { foo: Some("".into()), bar: Some(Some("".into())) }); 218 | check(["", "--foo=a", "--bar=b"], &Cli { foo: Some("a".into()), bar: Some(Some("b".into())) }); 219 | 220 | // `Option` makes the argument itself optional, but the value is required. 221 | check_err::( 222 | ["", "--foo"], 223 | expect!["a value is required for '--foo ' but none was supplied"], 224 | ); 225 | 226 | // `Option>` makes the argument and its value both optional. 227 | check(["", "--bar"], &Cli { foo: None, bar: Some(None) }); 228 | // It never consume the next argument. 229 | check_err::(["", "--bar", "value"], expect!["unexpected argument 'value'"]); 230 | } 231 | 232 | #[test] 233 | fn default_values() { 234 | #[derive(Debug, Clone, Default, PartialEq, Parser)] 235 | struct Cli { 236 | #[arg(short = 'O', default_value_t)] 237 | opt: i32, 238 | #[arg(long, default_value = "none", conflicts_with = "opt")] 239 | debug: String, 240 | } 241 | 242 | // Constraints are validated before default values. 243 | check([""], &Cli { opt: 0, debug: "none".into() }); 244 | check(["", "-O2"], &Cli { opt: 2, debug: "none".into() }); 245 | check(["", "--debug=full"], &Cli { opt: 0, debug: "full".into() }); 246 | 247 | check_err::( 248 | ["", "-O2", "--debug="], 249 | expect!["the argument '--debug ' cannot be used with some other arguments"], 250 | ); 251 | } 252 | 253 | #[test] 254 | fn flatten() { 255 | #[derive(Debug, PartialEq, Parser)] 256 | struct Cli { 257 | #[arg(long)] 258 | verbose: bool, 259 | #[command(flatten)] 260 | config: Config, 261 | #[arg(long)] 262 | debug: bool, 263 | } 264 | 265 | #[derive(Debug, PartialEq, Args)] 266 | struct Config { 267 | #[arg(long)] 268 | config: Option, 269 | #[arg(long)] 270 | config_file: Option, 271 | #[arg(short, required = true)] 272 | force: bool, 273 | } 274 | 275 | check( 276 | ["", "--debug", "--verbose", "--config", "a=b", "-f"], 277 | &Cli { 278 | debug: true, 279 | verbose: true, 280 | config: Config { config: Some("a=b".into()), config_file: None, force: true }, 281 | }, 282 | ); 283 | check( 284 | ["", "--config-file", "path", "--debug", "--verbose", "-f"], 285 | &Cli { 286 | debug: true, 287 | verbose: true, 288 | config: Config { config_file: Some("path".into()), config: None, force: true }, 289 | }, 290 | ); 291 | 292 | // Check for arg index offsets. 293 | 294 | // Value errors. 295 | check_err::( 296 | ["", "--config"], 297 | expect!["a value is required for '--config ' but none was supplied"], 298 | ); 299 | // Validation errors. 300 | check_err::( 301 | ["", "--config", "foo"], 302 | expect!["the argument '-f' is required but not provided"], 303 | ); 304 | } 305 | 306 | #[test] 307 | fn unknown_args() { 308 | #[derive(Debug, PartialEq, Parser)] 309 | struct Cli { 310 | #[arg(short, long)] 311 | verbose: bool, 312 | file: String, 313 | } 314 | 315 | #[derive(Debug, PartialEq, Parser)] 316 | struct CliWithSub { 317 | #[arg(long)] 318 | verbose: bool, 319 | file: String, 320 | #[command(subcommand)] 321 | sub: Sub, 322 | } 323 | 324 | check_err::(["", "--debug"], expect!["unexpected argument '--debug'"]); 325 | check_err::(["", "-d"], expect!["unexpected argument '-d'"]); 326 | check_err::(["", "-vd"], expect!["unexpected argument '-d'"]); 327 | check_err::(["", "-v坏"], expect!["unexpected argument '-坏'"]); 328 | 329 | check_err::(["", "path1", "path2"], expect!["unexpected argument 'path2'"]); 330 | 331 | check_err::(["", "path1", "path2"], expect!["unrecognized subcommand 'path2'"]); 332 | check_err::(["", "sub", "path"], expect!["unexpected argument 'path'"]); 333 | } 334 | 335 | #[cfg(unix)] 336 | #[test] 337 | fn non_utf8() { 338 | use std::ffi::OsStr; 339 | use std::os::unix::ffi::{OsStrExt, OsStringExt}; 340 | use std::path::PathBuf; 341 | 342 | #[derive(Debug, PartialEq, Parser)] 343 | struct Cli { 344 | #[arg(short, long)] 345 | config: Option, 346 | file: Option, 347 | #[arg(short)] 348 | verbose: bool, 349 | } 350 | 351 | fn concat_os(a: impl AsRef, b: impl AsRef) -> OsString { 352 | let mut buf = a.as_ref().as_bytes().to_vec(); 353 | buf.extend_from_slice(b.as_ref().as_bytes()); 354 | OsString::from_vec(buf) 355 | } 356 | 357 | let non_utf8 = || OsString::from_vec(vec![0xFF]); 358 | assert!(non_utf8().to_str().is_none()); 359 | check( 360 | ["".into(), non_utf8()], 361 | &Cli { file: Some(non_utf8().into()), config: None, verbose: false }, 362 | ); 363 | 364 | let mut exp = Cli { config: Some(non_utf8().into()), file: None, verbose: false }; 365 | check([OsString::new(), "--config".into(), non_utf8()], &exp); 366 | check([OsString::new(), concat_os("--config=", non_utf8())], &exp); 367 | check([OsString::new(), concat_os("-c", non_utf8())], &exp); 368 | check([OsString::new(), concat_os("-c=", non_utf8())], &exp); 369 | 370 | exp.verbose = true; 371 | check([OsString::new(), concat_os("-vc", non_utf8())], &exp); 372 | check([OsString::new(), concat_os("-vc=", non_utf8())], &exp); 373 | } 374 | 375 | #[test] 376 | fn global() { 377 | #[derive(Debug, PartialEq, Parser)] 378 | struct Cli { 379 | // Not global. 380 | #[arg(short)] 381 | verbose: bool, 382 | #[arg(short, long, global = true)] 383 | debug: Option, 384 | #[command(subcommand)] 385 | sub: Sub2, 386 | } 387 | 388 | #[derive(Debug, PartialEq, Subcommand)] 389 | enum Sub2 { 390 | Empty, 391 | Deep { 392 | #[arg(short)] 393 | verbose: bool, 394 | }, 395 | } 396 | 397 | check(["", "empty"], &Cli { verbose: false, debug: None, sub: Sub2::Empty }); 398 | check(["", "-v", "empty"], &Cli { verbose: true, debug: None, sub: Sub2::Empty }); 399 | 400 | // Not global. 401 | check_err::(["", "empty", "-v"], expect!["unexpected argument '-v'"]); 402 | 403 | // TODO: Is this behavior expected? 404 | check( 405 | ["", "-v", "deep"], 406 | &Cli { verbose: true, debug: None, sub: Sub2::Deep { verbose: false } }, 407 | ); 408 | check( 409 | ["", "deep", "-v"], 410 | &Cli { verbose: false, debug: None, sub: Sub2::Deep { verbose: true } }, 411 | ); 412 | check( 413 | ["", "-v", "deep", "-v"], 414 | &Cli { verbose: true, debug: None, sub: Sub2::Deep { verbose: true } }, 415 | ); 416 | 417 | check(["", "-d2", "empty"], &Cli { verbose: false, debug: Some(2), sub: Sub2::Empty }); 418 | check(["", "-d", "2", "empty"], &Cli { verbose: false, debug: Some(2), sub: Sub2::Empty }); 419 | check( 420 | ["", "-d2", "deep"], 421 | &Cli { verbose: false, debug: Some(2), sub: Sub2::Deep { verbose: false } }, 422 | ); 423 | check( 424 | ["", "-d", "2", "deep"], 425 | &Cli { verbose: false, debug: Some(2), sub: Sub2::Deep { verbose: false } }, 426 | ); 427 | 428 | check_err::( 429 | ["", "-d2", "deep", "-d0"], 430 | expect!["the argument '-d, --debug ' cannot be used multiple times"], 431 | ); 432 | } 433 | 434 | #[test] 435 | fn hyphen_named() { 436 | #[derive(Debug, Default, PartialEq, Parser)] 437 | struct Cli { 438 | #[arg(long)] 439 | no: Option, 440 | #[arg(long, allow_hyphen_values = true)] 441 | yes: Option, 442 | #[arg(long, allow_negative_numbers = true)] 443 | number: Option, 444 | 445 | #[arg(short = '1')] 446 | one: bool, 447 | #[arg(short)] 448 | flag: bool, 449 | } 450 | 451 | check_err::( 452 | ["", "--no", "-1"], 453 | expect!["a value is required for '--no ' but none was supplied"], 454 | ); 455 | check_err::( 456 | ["", "--no", "-f"], 457 | expect!["a value is required for '--no ' but none was supplied"], 458 | ); 459 | 460 | check_err::( 461 | ["", "--number", "-f"], 462 | expect!["a value is required for '--number ' but none was supplied"], 463 | ); 464 | check(["", "--number", "-1"], &Cli { number: Some(-1), ..Cli::default() }); 465 | 466 | check(["", "--yes", "-1"], &Cli { yes: Some("-1".into()), ..Cli::default() }); 467 | check(["", "--yes", "-f"], &Cli { yes: Some("-f".into()), ..Cli::default() }); 468 | } 469 | 470 | #[test] 471 | fn trailing_args() { 472 | #[derive(Debug, Default, PartialEq, Parser)] 473 | struct No { 474 | #[arg(short, default_value_t)] 475 | debug: i8, 476 | #[arg(trailing_var_arg = true)] 477 | any: Vec, 478 | } 479 | 480 | #[derive(Debug, Default, PartialEq, Parser)] 481 | struct Yes { 482 | #[arg(short, default_value_t, allow_hyphen_values = true)] 483 | debug: i8, 484 | #[arg(trailing_var_arg = true)] 485 | // TODO: allow_hyphen_values 486 | any: Vec, 487 | } 488 | 489 | // Implicit allow_hyphen_values when already entered a trailing_var_args. 490 | check( 491 | ["", "-d", "1", "a", "-d", "-1"], 492 | &No { debug: 1, any: vec!["a".into(), "-d".into(), "-1".into()] }, 493 | ); 494 | check_err::( 495 | ["", "-d", "-1"], 496 | expect!["a value is required for '-d ' but none was supplied"], 497 | ); 498 | check_err::(["", "-x"], expect!["unexpected argument '-x'"]); 499 | 500 | check(["", "-d", "-1"], &Yes { any: vec![], debug: -1 }); 501 | // TODO: check(["", "-x", "-d", "-1"], &Yes { any: vec!["-x".into(), "-d".into(), "-1".into()], debug: 0 }); 502 | } 503 | 504 | #[test] 505 | fn value_delimiter() { 506 | #[derive(Debug, Default, PartialEq, Parser)] 507 | struct Cli { 508 | #[arg(short = 'F', long, use_value_delimiter = true)] 509 | features: Vec, 510 | } 511 | 512 | check( 513 | ["", "--features", "a,b", "-F", "c", "-F=d,e", "--features="], 514 | &Cli { features: ["a", "b", "c", "d", "e", ""].map(Into::into).into() }, 515 | ); 516 | } 517 | 518 | #[test] 519 | fn constraint() { 520 | #[derive(Debug, Default, PartialEq, Parser)] 521 | struct Required { 522 | #[arg(long, required = true)] 523 | key: Vec, 524 | #[arg(required = true)] 525 | files: Vec, 526 | #[arg(short, required = true)] 527 | force: bool, 528 | #[arg(short, required = true)] 529 | level: Option, 530 | } 531 | 532 | check_err::([""], expect!["the argument '...' is required but not provided"]); 533 | check_err::( 534 | ["", "--key=foo"], 535 | expect!["the argument '...' is required but not provided"], 536 | ); 537 | check_err::( 538 | ["", "--key=foo", "path"], 539 | expect!["the argument '-f' is required but not provided"], 540 | ); 541 | check_err::( 542 | ["", "--key=foo", "path", "-f"], 543 | expect!["the argument '-l ' is required but not provided"], 544 | ); 545 | 546 | check( 547 | ["", "--key=foo", "path", "-fl2"], 548 | &Required { 549 | key: vec!["foo".into()], 550 | files: vec!["path".into()], 551 | force: true, 552 | level: Some(2), 553 | }, 554 | ) 555 | } 556 | 557 | #[test] 558 | #[deny(unreachable_code)] 559 | fn empty_subcommand() { 560 | #[derive(Debug, Parser, PartialEq)] 561 | struct Cli { 562 | #[command(subcommand)] 563 | cmd: Option, 564 | } 565 | 566 | #[derive(Debug, Subcommand, PartialEq)] 567 | enum Subcmd {} 568 | 569 | check([""], &Cli { cmd: None }); 570 | check_err::(["", ""], expect!["unrecognized subcommand ''"]); 571 | } 572 | -------------------------------------------------------------------------------- /derive/src/common.rs: -------------------------------------------------------------------------------- 1 | use std::ops; 2 | 3 | use proc_macro2::{Ident, Span, TokenStream}; 4 | use quote::{ToTokens, quote}; 5 | use syn::meta::ParseNestedMeta; 6 | use syn::parse::{Parse, ParseStream}; 7 | use syn::spanned::Spanned; 8 | use syn::{Attribute, GenericArgument, LitBool, LitChar, LitStr, PathArguments, Type}; 9 | use syn::{Token, bracketed, token}; 10 | 11 | use crate::error::try_syn; 12 | use crate::shared::ArgAttrs; 13 | 14 | pub const TY_BOOL: &str = "bool"; 15 | pub const TY_OPTION: &str = "Option"; 16 | pub const TY_VEC: &str = "Vec"; 17 | 18 | /// `TyCtor` => `ArgTy`. `ty_ctor` must be a single-identifier path. 19 | /// 20 | /// This is matched literally and does NOT try to be smart, i.e. 21 | /// it does not recognize absolute paths or unwrap parenthesis. 22 | pub fn strip_ty_ctor<'i>(ty: &'i Type, ty_ctor: &str) -> Option<&'i Type> { 23 | if let Type::Path(syn::TypePath { qself: None, path }) = ty 24 | && path.leading_colon.is_none() 25 | && path.segments.len() == 1 26 | { 27 | let seg = &path.segments[0]; 28 | if seg.ident == ty_ctor 29 | && let PathArguments::AngleBracketed(args) = &seg.arguments 30 | && args.args.len() == 1 31 | && let GenericArgument::Type(arg_ty) = &args.args[0] 32 | { 33 | return Some(arg_ty); 34 | } 35 | } 36 | None 37 | } 38 | 39 | pub fn wrap_anon_item(tts: impl ToTokens) -> TokenStream { 40 | quote! { 41 | const _: () = { 42 | use ::palc::__private as __rt; 43 | #tts 44 | }; 45 | } 46 | } 47 | 48 | // Utility macros for attribute parsers. 49 | 50 | trait OptionExt { 51 | fn set_once(&mut self, span: Span, v: T); 52 | } 53 | impl OptionExt for Option { 54 | fn set_once(&mut self, span: Span, v: T) { 55 | if self.is_none() { 56 | *self = Some(v); 57 | } else { 58 | emit_error!(span, "duplicated attribute"); 59 | } 60 | } 61 | } 62 | 63 | trait BoolExt { 64 | fn parse_true(&mut self, meta: &ParseNestedMeta<'_>) -> syn::Result<()>; 65 | } 66 | impl BoolExt for bool { 67 | fn parse_true(&mut self, meta: &ParseNestedMeta<'_>) -> syn::Result<()> { 68 | let lit = meta.value()?.parse::()?; 69 | if !lit.value { 70 | emit_error!(lit, "only `true` is supported here"); 71 | } else if *self { 72 | emit_error!(lit, "duplicated attribute"); 73 | } else { 74 | *self = true; 75 | } 76 | Ok(()) 77 | } 78 | } 79 | 80 | pub enum ArgOrCommand { 81 | Arg(Box), 82 | Command(ArgsCommandMeta), 83 | } 84 | 85 | impl ArgOrCommand { 86 | pub fn parse_attrs(attrs: &[Attribute]) -> ArgOrCommand { 87 | let mut doc = Doc::default(); 88 | let mut arg = None::>; 89 | let mut command = None; 90 | for attr in attrs { 91 | let path = attr.path(); 92 | doc.extend_from_attr(attr); 93 | if path.is_ident("arg") { 94 | let arg = arg.get_or_insert_default(); 95 | try_syn(attr.parse_nested_meta(|meta| arg.parse_update(&meta))); 96 | } else if path.is_ident("command") 97 | && let Some(c) = try_syn(attr.parse_args::()) 98 | { 99 | if command.is_some() { 100 | emit_error!(path, "duplicated command(..)"); 101 | } 102 | command = Some((path.span(), c)); 103 | } 104 | } 105 | 106 | doc.post_process(); 107 | 108 | if let Some((span, c)) = command { 109 | if arg.is_some() { 110 | emit_error!(span, "command(..) conflicts with arg(..)"); 111 | } 112 | Self::Command(c) 113 | } else { 114 | let mut arg = arg.unwrap_or_default(); 115 | arg.doc = doc; 116 | Self::Arg(arg) 117 | } 118 | } 119 | } 120 | 121 | #[derive(Default)] 122 | pub struct ArgMeta { 123 | pub doc: Doc, 124 | 125 | // Names. 126 | pub long: Option>, 127 | pub short: Option>, 128 | pub alias: OneOrArray, 129 | pub short_alias: OneOrArray, 130 | pub value_name: Option, 131 | // TODO: {,visible_}{,short_}alias{,es}, value_names 132 | 133 | // Named argument behaviors. 134 | pub require_equals: bool, 135 | pub global: bool, 136 | pub allow_hyphen_values: bool, 137 | pub allow_negative_numbers: bool, 138 | pub ignore_case: bool, 139 | 140 | // Unnamed argument behaviors. 141 | pub trailing_var_arg: bool, 142 | pub last: bool, 143 | // TODO: raw 144 | 145 | // Value behaviors. 146 | pub default_value: Option, 147 | pub default_value_t: Option>, 148 | pub value_delimiter: Option, 149 | pub value_enum: bool, 150 | // TODO: num_args, value_hint 151 | // index, action, value_terminator, default_missing_value*, env 152 | 153 | // Help & completion. 154 | pub help: Option, 155 | pub long_help: Option, 156 | pub hide: bool, 157 | // TODO: add, hide_*, next_line_help, help_heading, display_order 158 | 159 | // Validation. 160 | pub required: bool, 161 | pub exclusive: bool, 162 | pub requires: Vec, 163 | pub conflicts_with: Vec, 164 | // TODO: default_value_if{,s}, required_unless_present*, required_if*, 165 | // conflicts_with*, overrides_with* 166 | } 167 | 168 | impl ArgMeta { 169 | pub fn is_named(&self) -> bool { 170 | self.long.is_some() || self.short.is_some() 171 | } 172 | 173 | fn parse_update(&mut self, meta: &ParseNestedMeta<'_>) -> syn::Result<()> { 174 | let path = &meta.path; 175 | let span = path.span(); 176 | 177 | if path.is_ident("long") { 178 | self.long.set_once(span, meta.input.parse()?); 179 | } else if path.is_ident("short") { 180 | self.short.set_once(span, meta.input.parse()?); 181 | } else if path.is_ident("alias") || path.is_ident("aliases") { 182 | self.alias.extend(meta.value()?.parse::>()?); 183 | } else if path.is_ident("short_alias") || path.is_ident("short_aliases") { 184 | self.short_alias.extend(meta.value()?.parse::>()?); 185 | } else if path.is_ident("value_name") { 186 | self.value_name.set_once(span, meta.value()?.parse::()?); 187 | } else if path.is_ident("require_equals") { 188 | self.require_equals.parse_true(meta)?; 189 | } else if path.is_ident("global") { 190 | self.global.parse_true(meta)?; 191 | } else if path.is_ident("allow_hyphen_values") { 192 | self.allow_hyphen_values.parse_true(meta)?; 193 | } else if path.is_ident("allow_negative_numbers") { 194 | self.allow_negative_numbers.parse_true(meta)?; 195 | } else if path.is_ident("trailing_var_arg") { 196 | self.trailing_var_arg.parse_true(meta)?; 197 | } else if path.is_ident("last") { 198 | self.last.parse_true(meta)?; 199 | } else if path.is_ident("default_value") { 200 | self.default_value.set_once(span, meta.value()?.parse()?); 201 | } else if path.is_ident("default_value_t") || path.is_ident("default_values_t") { 202 | self.default_value_t.set_once(span, meta.input.parse()?); 203 | } else if path.is_ident("use_value_delimiter") { 204 | let lit = meta.value()?.parse::()?; 205 | if !lit.value { 206 | emit_error!(lit, "only `true` is supported here"); 207 | } 208 | self.value_delimiter.set_once(span, syn::LitChar::new(',', Span::call_site())); 209 | } else if path.is_ident("value_delimiter") { 210 | self.value_delimiter.set_once(span, meta.value()?.parse::()?); 211 | } else if path.is_ident("value_enum") { 212 | // NB. This attribute is standalone without `=`. 213 | if self.value_enum { 214 | emit_error!(path, "duplicated attribute"); 215 | } 216 | self.value_enum = true; 217 | } else if path.is_ident("ignore_case") { 218 | self.ignore_case.parse_true(meta)?; 219 | } else if path.is_ident("help") { 220 | self.help.set_once(span, meta.value()?.parse::()?); 221 | } else if path.is_ident("long_help") { 222 | self.long_help.set_once(span, meta.value()?.parse::()?); 223 | } else if path.is_ident("hide") { 224 | self.hide.parse_true(meta)?; 225 | } else if path.is_ident("required") { 226 | self.required.parse_true(meta)?; 227 | } else if path.is_ident("exclusive") { 228 | self.exclusive.parse_true(meta)?; 229 | } else if path.is_ident("requires") { 230 | self.requires.push(meta.value()?.parse()?); 231 | } else if path.is_ident("conflicts_with") { 232 | self.conflicts_with.push(meta.value()?.parse()?); 233 | } else if path.is_ident("conflicts_with_all") { 234 | self.conflicts_with.extend(meta.value()?.parse::>()?); 235 | } else { 236 | emit_error!(path, "unknown attribute"); 237 | } 238 | Ok(()) 239 | } 240 | } 241 | 242 | /// The inner `command(..)` on fields of `derive(Parser, Args, Subcommand)`. 243 | pub enum ArgsCommandMeta { 244 | Subcommand, 245 | Flatten, 246 | } 247 | 248 | impl Parse for ArgsCommandMeta { 249 | fn parse(input: ParseStream) -> syn::Result { 250 | let ident = input.parse::()?; 251 | Ok(if ident == "subcommand" { 252 | Self::Subcommand 253 | } else if ident == "flatten" { 254 | Self::Flatten 255 | } else { 256 | return Err(syn::Error::new(ident.span(), "must be either 'subcommand' or 'flatten'")); 257 | }) 258 | } 259 | } 260 | 261 | /// `command(..)` on the struct of `derive(Parser)` or enum variants of `derive(Subcommand)`. 262 | #[derive(Default)] 263 | pub struct CommandMeta { 264 | pub doc: Doc, 265 | 266 | pub name: Option, 267 | pub version: Option>, 268 | pub long_about: Option>, 269 | pub after_long_help: Option, 270 | // TODO: bin_name, verbatim_doc_comment, next_display_order, next_help_heading, rename_all{,_env} 271 | } 272 | 273 | impl CommandMeta { 274 | pub fn parse_attrs_opt(attrs: &[Attribute]) -> Option> { 275 | let mut doc = Doc::default(); 276 | let mut this: Option> = None; 277 | for attr in attrs { 278 | doc.extend_from_attr(attr); 279 | if attr.path().is_ident("command") { 280 | let this = this.get_or_insert_default(); 281 | try_syn(attr.parse_nested_meta(|meta| this.parse_update(&meta))); 282 | } else if attr.path().is_ident("arg") { 283 | emit_error!(attr, "only `command(..)` is allowed in this location"); 284 | } 285 | } 286 | doc.post_process(); 287 | if !doc.0.is_empty() { 288 | this.get_or_insert_default().doc = doc; 289 | } 290 | this 291 | } 292 | 293 | fn parse_update(&mut self, meta: &ParseNestedMeta<'_>) -> syn::Result<()> { 294 | let path = &meta.path; 295 | let span = path.span(); 296 | 297 | if path.is_ident("name") { 298 | self.name.set_once(span, meta.value()?.parse()?); 299 | } else if path.is_ident("version") { 300 | self.version.set_once(span, meta.input.parse()?); 301 | } else if path.is_ident("long_about") { 302 | self.long_about.set_once(span, meta.input.parse()?); 303 | } else if path.is_ident("after_long_help") { 304 | self.after_long_help.set_once(span, meta.value()?.parse()?); 305 | } else if path.is_ident("author") { 306 | meta.input.parse::>()?; 307 | emit_error!( 308 | span, 309 | "`command(author)` is NOT supported. \ 310 | It is useless without custom help template anyway." 311 | ); 312 | } else if path.is_ident("about") { 313 | meta.input.parse::>()?; 314 | emit_error!( 315 | span, 316 | "custom short-about `command(about)` is NOT supported yet. \ 317 | It always use the first line of `command(long_about)` (or doc-comments). \ 318 | Please use `command(long_about)` (or doc-comments) instead.", 319 | ); 320 | } else if path.is_ident("after_help") { 321 | meta.input.parse::>()?; 322 | emit_error!( 323 | span, 324 | "custom after-short-help `command(after_help)` is NOT supported yet. \ 325 | It always use the first line of `command(after_long_help)`. \ 326 | Please use `command(after_long_help)` instead.", 327 | ); 328 | } else if path.is_ident("term_width") || path.is_ident("max_term_width") { 329 | meta.value()?.parse::()?; 330 | emit_error!( 331 | span, 332 | "`command(term_width, max_term_width)` are intentionally NOT supported, \ 333 | because line-wrapping's drawback outweighs its benefits.", 334 | ); 335 | } else { 336 | emit_error!(span, "unknown attribute"); 337 | } 338 | 339 | Ok(()) 340 | } 341 | } 342 | 343 | /// Top-level `#[value]` for `derive(ValueEnum)` enum. 344 | pub struct ValueEnumMeta { 345 | pub rename_all: Rename, 346 | } 347 | 348 | impl ValueEnumMeta { 349 | pub fn parse_attrs(attrs: &[Attribute]) -> Self { 350 | let mut rename_all = None; 351 | for attr in attrs { 352 | if !attr.path().is_ident("value") { 353 | continue; 354 | } 355 | try_syn(attr.parse_nested_meta(|meta| { 356 | if meta.path.is_ident("rename_all") { 357 | rename_all.set_once(meta.path.span(), meta.value()?.parse::()?); 358 | } else { 359 | emit_error!(meta.path, "unknown attribute"); 360 | } 361 | Ok(()) 362 | })); 363 | } 364 | Self { rename_all: rename_all.unwrap_or(Rename::KebabCase) } 365 | } 366 | } 367 | 368 | /// Variant `#[value]` for `derive(ValueEnum)` enum. 369 | #[derive(Default)] 370 | pub struct ValueVariantMeta { 371 | pub name: Option, 372 | // TODO: skip, help 373 | } 374 | 375 | impl ValueVariantMeta { 376 | pub fn parse_attrs(attrs: &[Attribute]) -> Self { 377 | let mut this = Self::default(); 378 | for attr in attrs { 379 | if !attr.path().is_ident("value") { 380 | continue; 381 | } 382 | try_syn(attr.parse_nested_meta(|meta| { 383 | if meta.path.is_ident("name") { 384 | this.name.set_once(meta.path.span(), meta.value()?.parse::()?.value()); 385 | } else { 386 | emit_error!(meta.path, "unknown attribute"); 387 | } 388 | Ok(()) 389 | })); 390 | } 391 | this 392 | } 393 | } 394 | 395 | // Follows 396 | #[derive(Clone, Copy)] 397 | pub enum Rename { 398 | CamelCase, 399 | KebabCase, 400 | PascalCase, 401 | ScreamingSnakeCase, 402 | SnakeCase, 403 | Lower, 404 | Upper, 405 | Verbatim, 406 | } 407 | 408 | impl Parse for Rename { 409 | fn parse(input: ParseStream) -> syn::Result { 410 | let s = input.parse::()?; 411 | Ok(match &*s.value() { 412 | "camelCase" => Self::CamelCase, 413 | "kebab-case" => Self::KebabCase, 414 | "PascalCase" => Self::PascalCase, 415 | "SCREAMING_SNAKE_CASE" => Self::ScreamingSnakeCase, 416 | "snake_case" => Self::SnakeCase, 417 | "lower" => Self::Lower, 418 | "UPPER" => Self::Upper, 419 | "verbatim" => Self::Verbatim, 420 | _ => return Err(syn::Error::new(s.span(), "unknown case conversion")), 421 | }) 422 | } 423 | } 424 | 425 | impl Rename { 426 | pub fn rename(self, s: String) -> String { 427 | #[allow(clippy::wildcard_imports)] 428 | use heck::*; 429 | 430 | match self { 431 | Self::CamelCase => s.to_lower_camel_case(), 432 | Self::KebabCase => s.to_kebab_case(), 433 | Self::PascalCase => s.to_pascal_case(), 434 | Self::ScreamingSnakeCase => s.to_shouty_snake_case(), 435 | Self::SnakeCase => s.to_snake_case(), 436 | Self::Lower => s.to_lowercase(), 437 | Self::Upper => s.to_uppercase(), 438 | Self::Verbatim => s, 439 | } 440 | } 441 | } 442 | 443 | pub struct OneOrArray(pub Vec); 444 | 445 | impl Default for OneOrArray { 446 | fn default() -> Self { 447 | Self(Vec::new()) 448 | } 449 | } 450 | 451 | impl ops::Deref for OneOrArray { 452 | type Target = [T]; 453 | fn deref(&self) -> &Self::Target { 454 | &self.0 455 | } 456 | } 457 | 458 | impl IntoIterator for OneOrArray { 459 | type Item = T; 460 | type IntoIter = std::vec::IntoIter; 461 | fn into_iter(self) -> Self::IntoIter { 462 | self.0.into_iter() 463 | } 464 | } 465 | 466 | impl Extend for OneOrArray { 467 | fn extend>(&mut self, iter: I) { 468 | self.0.extend(iter); 469 | } 470 | } 471 | 472 | impl Parse for OneOrArray { 473 | fn parse(input: ParseStream) -> syn::Result { 474 | Ok(Self(if input.peek(token::Bracket) { 475 | let inner; 476 | bracketed!(inner in input); 477 | inner.parse_terminated(T::parse, Token![,])?.into_iter().collect() 478 | } else { 479 | vec![input.parse::()?] 480 | })) 481 | } 482 | } 483 | 484 | pub enum Override { 485 | Inherit, 486 | Explicit(T), 487 | } 488 | 489 | impl Parse for Override { 490 | fn parse(input: ParseStream) -> syn::Result { 491 | Ok(if input.peek(Token![=]) { 492 | input.parse::()?; 493 | Self::Explicit(input.parse()?) 494 | } else { 495 | Self::Inherit 496 | }) 497 | } 498 | } 499 | 500 | /// `"IDENT"` or `("." IDENT)+`. 501 | pub struct FieldPath(pub Vec); 502 | 503 | impl ops::Deref for FieldPath { 504 | type Target = [Ident]; 505 | 506 | fn deref(&self) -> &Self::Target { 507 | &self.0 508 | } 509 | } 510 | 511 | impl Parse for FieldPath { 512 | fn parse(input: ParseStream) -> syn::Result { 513 | let mut path = Vec::new(); 514 | if input.peek(LitStr) { 515 | path.push(input.parse::()?.parse::()?); 516 | } else { 517 | while input.peek(Token![.]) { 518 | input.parse::()?; 519 | path.push(input.parse::()?); 520 | } 521 | if path.is_empty() { 522 | return Err(input.error(r#"expecting "field" or .field"#)); 523 | } 524 | } 525 | Ok(Self(path)) 526 | } 527 | } 528 | 529 | impl ToTokens for FieldPath { 530 | fn to_tokens(&self, tokens: &mut TokenStream) { 531 | for ident in &self.0 { 532 | tokens.extend(quote! { . }); 533 | ident.to_tokens(tokens); 534 | } 535 | } 536 | } 537 | 538 | pub struct VerbatimExpr(TokenStream); 539 | 540 | impl Parse for VerbatimExpr { 541 | fn parse(input: ParseStream) -> syn::Result { 542 | Ok(Self(input.parse::()?.to_token_stream())) 543 | } 544 | } 545 | 546 | impl From for TokenStream { 547 | fn from(e: VerbatimExpr) -> Self { 548 | e.0 549 | } 550 | } 551 | 552 | impl ToTokens for VerbatimExpr { 553 | fn to_tokens(&self, tokens: &mut TokenStream) { 554 | self.0.to_tokens(tokens); 555 | } 556 | 557 | fn into_token_stream(self) -> TokenStream 558 | where 559 | Self: Sized, 560 | { 561 | self.0 562 | } 563 | } 564 | 565 | /// Collect doc-comments into a single string. 566 | /// 567 | /// Paragraph (consecutive doc-comments without blank lines) are joined with space. In the result, 568 | /// the first line is the summary, and each of rest lines corresponds to a paragraph. 569 | /// 570 | /// See `src/refl.rs` for runtime usage that depends on this. 571 | #[derive(Default, PartialEq)] 572 | pub struct Doc(pub String); 573 | 574 | impl Doc { 575 | fn post_process(&mut self) { 576 | let len = self.0.trim_ascii_end().len(); 577 | self.0.truncate(len); 578 | } 579 | 580 | fn extend_from_attr(&mut self, attr: &Attribute) { 581 | if !attr.path().is_ident("doc") { 582 | return; 583 | } 584 | let syn::Meta::NameValue(m) = &attr.meta else { return }; 585 | if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(s), .. }) = &m.value { 586 | let s = s.value(); 587 | let s = s.trim_ascii(); 588 | if s.is_empty() { 589 | if !self.0.ends_with('\n') { 590 | self.0.push('\n'); 591 | } 592 | } else { 593 | if !self.0.is_empty() && !self.0.ends_with('\n') { 594 | self.0.push(' '); 595 | } 596 | self.0.push_str(s); 597 | } 598 | } else { 599 | emit_error!(m.value, "only literal doc comment is supported yet"); 600 | } 601 | } 602 | } 603 | 604 | impl ToTokens for Doc { 605 | fn to_tokens(&self, tokens: &mut TokenStream) { 606 | self.0.to_tokens(tokens); 607 | } 608 | } 609 | 610 | impl ToTokens for ArgAttrs { 611 | fn to_tokens(&self, tokens: &mut TokenStream) { 612 | let val = self.0; 613 | tokens.extend(quote! { __rt::ArgAttrs(#val) }); 614 | } 615 | } 616 | --------------------------------------------------------------------------------