From 522f1850e2acc766f429fc2a087ee6624586b634 Mon Sep 17 00:00:00 2001 From: Ben Wiederhake Date: Sat, 30 Mar 2024 19:41:26 +0100 Subject: [PATCH 1/6] implement proof of concept for du -bkB --- tests/coreutils.rs | 3 + tests/coreutils/du.rs | 152 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+) create mode 100644 tests/coreutils/du.rs diff --git a/tests/coreutils.rs b/tests/coreutils.rs index eca5376..94ac4fc 100644 --- a/tests/coreutils.rs +++ b/tests/coreutils.rs @@ -22,6 +22,9 @@ mod date; #[path = "coreutils/dd.rs"] mod dd; +#[path = "coreutils/du.rs"] +mod du; + #[path = "coreutils/echo.rs"] mod echo; diff --git a/tests/coreutils/du.rs b/tests/coreutils/du.rs new file mode 100644 index 0000000..d076a82 --- /dev/null +++ b/tests/coreutils/du.rs @@ -0,0 +1,152 @@ +use std::ffi::OsString; +use uutils_args::{Arguments, Options}; + +#[derive(Arguments)] +enum Arg { + #[arg("--apparent-size")] + ApparentSize, + + #[arg("-B[SIZE]", "--block-size[=SIZE]")] + BlockSize(OsString), + + #[arg("-b", "--bytes")] + Bytes, + + #[arg("-k")] + KibiBytes, + + #[arg("-m")] + MibiBytes, + // Note that --si and -h only affect the *output formatting*, + // and not the size determination itself. +} + +#[derive(Debug, Default, PartialEq, Eq)] +struct Settings { + apparent_size: bool, + block_size_str: Option, +} + +impl Options for Settings { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { + match arg { + Arg::ApparentSize => self.apparent_size = true, + Arg::BlockSize(os_str) => self.block_size_str = Some(os_str), + Arg::Bytes => { + self.apparent_size = true; + self.block_size_str = Some("1".into()); + } + Arg::KibiBytes => self.block_size_str = Some("K".into()), + Arg::MibiBytes => self.block_size_str = Some("M".into()), + } + Ok(()) + } +} + +#[test] +fn noarg() { + let (settings, operands) = Settings::default().parse(["du"]).unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!( + settings, + Settings { + apparent_size: false, + block_size_str: None, + } + ); +} + +#[test] +fn bytes() { + let (settings, operands) = Settings::default().parse(["du", "-b"]).unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!( + settings, + Settings { + apparent_size: true, + block_size_str: Some("1".into()), + } + ); +} + +#[test] +fn kibibytes() { + let (settings, operands) = Settings::default().parse(["du", "-k"]).unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!( + settings, + Settings { + apparent_size: false, + block_size_str: Some("K".into()), + } + ); +} + +#[test] +fn bytes_kibibytes() { + let (settings, operands) = Settings::default().parse(["du", "-bk"]).unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!( + settings, + Settings { + apparent_size: true, + block_size_str: Some("K".into()), + } + ); +} + +#[test] +fn kibibytes_bytes() { + let (settings, operands) = Settings::default().parse(["du", "-kb"]).unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!( + settings, + Settings { + apparent_size: true, + block_size_str: Some("1".into()), + } + ); +} + +#[test] +fn apparent_size() { + let (settings, operands) = Settings::default() + .parse(["du", "--apparent-size"]) + .unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!( + settings, + Settings { + apparent_size: true, + block_size_str: None, + } + ); +} + +#[test] +fn mibibytes() { + let (settings, operands) = Settings::default().parse(["du", "-m"]).unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!( + settings, + Settings { + apparent_size: false, + block_size_str: Some("M".into()), + } + ); +} + +#[test] +fn all() { + let (settings, operands) = Settings::default() + .parse(["du", "--apparent-size", "-bkm", "-B123"]) + .unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!( + settings, + Settings { + apparent_size: true, + block_size_str: Some("123".into()), + } + ); +} From 6e5f7b7ea38722bcda967e446aac37fb3a2e5a15 Mon Sep 17 00:00:00 2001 From: Ben Wiederhake Date: Sat, 12 Apr 2025 22:20:32 +0200 Subject: [PATCH 2/6] remove 'complete' feature flag, make it unconditional This avoids using cfg with the destination-crate's feature "complete" in proc-macro "derive". Quoting cargo: using a cfg inside a derive macro will use the cfgs from the destination crate and not the ones from the defining crate In these two instances, the cfg was simply used to avoid emitting dead code if the "complete" feature is not desired. By passing the "complete" feature down to the derive crate and evaluating if there, we achieve the same goal, without having to deal with suprisingly-valued and unexpected cfgs downstream. Alternatively, we could just evaluate the flag inside the derive crate, and pass the feature flag to it. --- .github/workflows/ci.yml | 4 ++-- Cargo.toml | 5 ++--- Justfile | 4 ++-- derive/src/lib.rs | 2 -- docs/guide/completions.md | 2 +- src/lib.rs | 2 -- src/value.rs | 4 ---- 7 files changed, 7 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 29a2eae..ee5dafd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - - run: cargo test --features complete --workspace + - run: cargo test --workspace rustfmt: name: Rustfmt @@ -48,7 +48,7 @@ jobs: components: clippy - uses: Swatinem/rust-cache@v2 - name: Clippy check - run: cargo clippy --all-targets --features complete --workspace -- -D warnings + run: cargo clippy --all-targets --workspace -- -D warnings docs: name: Docs diff --git a/Cargo.toml b/Cargo.toml index e9c6517..32a3905 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,13 +11,12 @@ readme = "README.md" [dependencies] uutils-args-derive = { version = "0.1.0", path = "derive" } -uutils-args-complete = { version = "0.1.0", path = "complete", optional = true } +uutils-args-complete = { version = "0.1.0", path = "complete" } strsim = "0.11.1" lexopt = "0.3.0" [features] -parse-is-complete = ["complete"] -complete = ["uutils-args-complete"] +parse-is-complete = [] [workspace] members = ["derive", "complete"] diff --git a/Justfile b/Justfile index 7790af0..c7cea4d 100644 --- a/Justfile +++ b/Justfile @@ -1,5 +1,5 @@ check: cargo fmt --all - cargo test --features complete - cargo clippy --all-targets --features complete --workspace -- -D warnings + cargo test + cargo clippy --all-targets --workspace -- -D warnings cargo doc diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 5f6d9d7..5b0aa79 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -117,7 +117,6 @@ pub fn arguments(input: TokenStream) -> TokenStream { #version_string } - #[cfg(feature = "complete")] fn complete() -> ::uutils_args_complete::Command<'static> { use ::uutils_args::Value; #complete_command @@ -212,7 +211,6 @@ pub fn value(input: TokenStream) -> TokenStream { }) } - #[cfg(feature = "complete")] fn value_hint() -> ::uutils_args_complete::ValueHint { let keys: [&str; #keys_len] = [#(#all_keys),*]; ::uutils_args_complete::ValueHint::Strings( diff --git a/docs/guide/completions.md b/docs/guide/completions.md index d89d3dd..e7d04c1 100644 --- a/docs/guide/completions.md +++ b/docs/guide/completions.md @@ -39,7 +39,7 @@ The `[shell]` value here can be `fish`, `zsh`, `nu`, `sh`, `bash`, `csh`, `elvis Additionally, the values `man` or `md` can be passed to generate man pages and markdown documentation (for `mdbook`). -If you do not want to hijack the [`Options::parse`](crate::Options::parse) function, you can instead enable the `complete` feature flag. This makes the `Options::complete` function available in addition to the [`Options::parse`](crate::Options::parse) function to generate a `String` with the completion. +If you do not want to hijack the [`Options::parse`](crate::Options::parse) function, you can instead use the `Options::complete` function available in addition to the [`Options::parse`](crate::Options::parse) function to generate a `String` with the completion.
diff --git a/src/lib.rs b/src/lib.rs index e7b7058..e2c9cce 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -103,7 +103,6 @@ pub trait Arguments: Sized { Ok(()) } - #[cfg(feature = "complete")] fn complete() -> uutils_args_complete::Command<'static>; } @@ -197,7 +196,6 @@ pub trait Options: Sized { } } - #[cfg(feature = "complete")] fn complete(shell: &str) -> String { uutils_args_complete::render(&Arg::complete(), shell) } diff --git a/src/value.rs b/src/value.rs index 54cfbba..2109a6a 100644 --- a/src/value.rs +++ b/src/value.rs @@ -6,7 +6,6 @@ use std::{ ffi::{OsStr, OsString}, path::PathBuf, }; -#[cfg(feature = "complete")] use uutils_args_complete::ValueHint; pub type ValueResult = Result>; @@ -54,7 +53,6 @@ impl std::fmt::Display for ValueError { pub trait Value: Sized { fn from_value(value: &OsStr) -> ValueResult; - #[cfg(feature = "complete")] fn value_hint() -> ValueHint { ValueHint::Unknown } @@ -71,7 +69,6 @@ impl Value for PathBuf { Ok(PathBuf::from(value)) } - #[cfg(feature = "complete")] fn value_hint() -> ValueHint { ValueHint::AnyPath } @@ -98,7 +95,6 @@ where Ok(Some(T::from_value(value)?)) } - #[cfg(feature = "complete")] fn value_hint() -> uutils_args_complete::ValueHint { T::value_hint() } From b2e78d3f58c872f6a6bd75312c3026ef69738dd6 Mon Sep 17 00:00:00 2001 From: Ben Wiederhake Date: Sun, 13 Apr 2025 20:53:39 +0200 Subject: [PATCH 3/6] derive: make error messages slightly more readable, add manually-driven test --- derive/src/argument.rs | 3 +- derive/src/flags.rs | 11 ++-- derive/src/help.rs | 8 ++- derive/src/lib.rs | 3 +- examples/test_compile_errors_manually.rs | 84 ++++++++++++++++++++++++ 5 files changed, 99 insertions(+), 10 deletions(-) create mode 100644 examples/test_compile_errors_manually.rs diff --git a/derive/src/argument.rs b/derive/src/argument.rs index 94b84eb..7a09b2b 100644 --- a/derive/src/argument.rs +++ b/derive/src/argument.rs @@ -40,7 +40,8 @@ pub fn parse_arguments_attr(attrs: &[Attribute]) -> ArgumentsAttr { pub fn parse_argument(v: Variant) -> Vec { let ident = v.ident; - let attributes = get_arg_attributes(&v.attrs).unwrap(); + let attributes = get_arg_attributes(&v.attrs) + .expect("can't parse arg attributes, expected one or more strings"); // Return early because we don't need to check the fields if it's not used. if attributes.is_empty() { diff --git a/derive/src/flags.rs b/derive/src/flags.rs index 3d3db27..da7497e 100644 --- a/derive/src/flags.rs +++ b/derive/src/flags.rs @@ -60,8 +60,9 @@ impl Flags { } else if sep == '[' { let optional = val .strip_prefix('=') - .and_then(|s| s.strip_suffix(']')) - .unwrap(); + .expect("expected '=' after '[' in flag pattern") + .strip_suffix(']') + .expect("expected final ']' in flag pattern"); assert!( optional .chars() @@ -74,8 +75,6 @@ impl Flags { self.long.push(Flag { flag: f, value }); } else if let Some(s) = flag.strip_prefix('-') { - assert!(!s.is_empty()); - // There are three possible patterns: // -f // -f value @@ -83,7 +82,9 @@ impl Flags { // First we trim up to the = or [ let mut chars = s.chars(); - let f = chars.next().unwrap(); + let f = chars + .next() + .expect("flag name must be non-empty (cannot be just '-')"); let val: String = chars.collect(); // Now check the cases: diff --git a/derive/src/help.rs b/derive/src/help.rs index 77af149..2c99c3a 100644 --- a/derive/src/help.rs +++ b/derive/src/help.rs @@ -100,12 +100,14 @@ pub fn help_string( pub fn read_help_file(file: &str) -> (String, String, String) { let path = Path::new(file); - let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + let manifest_dir = + std::env::var("CARGO_MANIFEST_DIR").expect("can only run in paths that are valid UTF-8"); let mut location = PathBuf::from(manifest_dir); location.push(path); let mut contents = String::new(); - let mut f = std::fs::File::open(location).unwrap(); - f.read_to_string(&mut contents).unwrap(); + let mut f = std::fs::File::open(location).expect("cannot open help-string file"); + f.read_to_string(&mut contents) + .expect("cannot read from help-string file"); ( parse_about(&contents), diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 5f6d9d7..cf4fa15 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -152,7 +152,8 @@ pub fn value(input: TokenStream) -> TokenStream { continue; } - let ValueAttr { keys, value } = ValueAttr::parse(&attr).unwrap(); + let ValueAttr { keys, value } = + ValueAttr::parse(&attr).expect("expected comma-separated list of string literals"); let keys = if keys.is_empty() { vec![variant_name.to_lowercase()] diff --git a/examples/test_compile_errors_manually.rs b/examples/test_compile_errors_manually.rs new file mode 100644 index 0000000..7d08f10 --- /dev/null +++ b/examples/test_compile_errors_manually.rs @@ -0,0 +1,84 @@ +use uutils_args::{Arguments, Options, Value}; + +// Using a fully-fledged compile-error testsuite is a bit overkill, but we still +// want to make sure that the `derive` crate generates reasonable error messages. +// That's what this "example" is for. In the following, there are blocks of +// lines, one marked as POSITIVE and multiple lines marked as NEGATIVE. The +// committed version of this file should only contain POSITIVE. In order to run a +// test, comment out the POSITIVE line, and use a NEGATIVE line instead, and +// manually check whether you see a reasonable error message – ideally the error +// message indicated by the comment. One way to do this is: + +// $ cargo build --example test_compile_errors_manually + +#[derive(Value, Debug, Default)] +enum Flavor { + #[default] + #[value("kind", "nice")] + Kind, + #[value("condescending")] // POSITIVE + // #[value(condescending)] // NEGATIVE: "expected comma-separated list of string literals" + Condescending, +} + +#[derive(Arguments)] +#[arguments(file = "examples/hello_world_help.md")] // POSITIVE +// #[arguments(file = "examples/nonexistent.md")] // NEGATIVE: "cannot open help-string file" +// #[arguments(file = "/dev/full")] // NEGATIVE: Causes OOM, FIXME +// #[arguments(file = "/")] // NEGATIVE: "cannot read from help-string file" +// #[arguments(file = "path/to/some/WRITE-ONLY/file")] // NEGATIVE: "cannot open help-string file" +enum Arg { + /// The name to greet + #[arg("-n NAME", "--name[=NAME]", "name=NAME")] // POSITIVE + // #[arg("-")] // NEGATIVE: flag name must be non-empty (cannot be just '-') + // #[arg("-n NAME", "--name[NAME]", "name=NAME")] // NEGATIVE: "expected '=' after '[' in flag pattern" + // #[arg("-n NAME", "--name[=NAME", "name=NAME")] // NEGATIVE: "expected final ']' in flag pattern" + // #[arg(key="name")] // NEGATIVE: "can't parse arg attributes, expected one or more strings" + Name(String), + + /// The number of times to greet + #[arg("-c N", "--count=N")] + Count(u8), + + #[arg("--flavor=FLAVOR")] + Flavor(Flavor), +} + +struct Settings { + name: String, + count: u8, + flavor: Flavor, +} + +impl Options for Settings { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { + match arg { + Arg::Name(n) => self.name = n, + Arg::Count(c) => self.count = c, + Arg::Flavor(flavor) => self.flavor = flavor, + } + Ok(()) + } +} + +fn main() -> Result<(), uutils_args::Error> { + let (settings, _operands) = Settings { + name: String::new(), + count: 1, + flavor: Flavor::Kind, + } + .parse(std::env::args_os()) + .unwrap(); + + for _ in 0..settings.count { + match settings.flavor { + Flavor::Kind => { + println!("Hello, {}!", settings.name); + } + Flavor::Condescending => { + println!("Ugh, {}.", settings.name); + } + } + } + Ok(()) +} From bbb193d3870fb5b564b1189bc80d4df8b3f65280 Mon Sep 17 00:00:00 2001 From: Ben Wiederhake Date: Wed, 16 Apr 2025 02:12:55 +0200 Subject: [PATCH 4/6] make 'complete' available to procmacro by conversion to module --- Cargo.toml | 8 +++---- complete/Cargo.toml | 11 --------- complete/LICENSE | 1 - derive/src/complete.rs | 28 ++++++++++++++-------- derive/src/lib.rs | 6 ++--- {complete/src => src/complete}/bash.rs | 4 ++-- {complete/src => src/complete}/fish.rs | 4 ++-- {complete/src => src/complete}/man.rs | 2 +- {complete/src => src/complete}/md.rs | 2 +- complete/src/lib.rs => src/complete/mod.rs | 0 {complete/src => src/complete}/nu.rs | 2 +- {complete/src => src/complete}/zsh.rs | 2 +- src/lib.rs | 5 ++-- src/value.rs | 4 ++-- 14 files changed, 38 insertions(+), 41 deletions(-) delete mode 100644 complete/Cargo.toml delete mode 120000 complete/LICENSE rename {complete/src => src/complete}/bash.rs (97%) rename {complete/src => src/complete}/fish.rs (97%) rename {complete/src => src/complete}/man.rs (97%) rename {complete/src => src/complete}/md.rs (97%) rename complete/src/lib.rs => src/complete/mod.rs (100%) rename {complete/src => src/complete}/nu.rs (97%) rename {complete/src => src/complete}/zsh.rs (98%) diff --git a/Cargo.toml b/Cargo.toml index 32a3905..6adaff6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,13 +10,13 @@ repository = "https://github.com/uutils/uutils-args" readme = "README.md" [dependencies] -uutils-args-derive = { version = "0.1.0", path = "derive" } -uutils-args-complete = { version = "0.1.0", path = "complete" } -strsim = "0.11.1" lexopt = "0.3.0" +roff = "0.2.1" +strsim = "0.11.1" +uutils-args-derive = { version = "0.1.0", path = "derive" } [features] parse-is-complete = [] [workspace] -members = ["derive", "complete"] +members = ["derive"] diff --git a/complete/Cargo.toml b/complete/Cargo.toml deleted file mode 100644 index c9bae20..0000000 --- a/complete/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "uutils-args-complete" -version = "0.1.0" -edition = "2024" -authors = ["Terts Diepraam"] -license = "MIT" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -roff = "0.2.1" diff --git a/complete/LICENSE b/complete/LICENSE deleted file mode 120000 index ea5b606..0000000 --- a/complete/LICENSE +++ /dev/null @@ -1 +0,0 @@ -../LICENSE \ No newline at end of file diff --git a/derive/src/complete.rs b/derive/src/complete.rs index 10b9691..9e919d1 100644 --- a/derive/src/complete.rs +++ b/derive/src/complete.rs @@ -49,11 +49,15 @@ pub fn complete(args: &[Argument], file: &Option) -> TokenStream { .map(|Flag { flag, value }| { let flag = flag.to_string(); let value = match value { - Value::No => quote!(::uutils_args_complete::Value::No), - Value::Optional(name) => quote!(::uutils_args_complete::Value::Optional(#name)), - Value::Required(name) => quote!(::uutils_args_complete::Value::Required(#name)), + Value::No => quote!(::uutils_args::complete::Value::No), + Value::Optional(name) => { + quote!(::uutils_args::complete::Value::Optional(#name)) + } + Value::Required(name) => { + quote!(::uutils_args::complete::Value::Required(#name)) + } }; - quote!(::uutils_args_complete::Flag { + quote!(::uutils_args::complete::Flag { flag: #flag, value: #value }) @@ -64,11 +68,15 @@ pub fn complete(args: &[Argument], file: &Option) -> TokenStream { .iter() .map(|Flag { flag, value }| { let value = match value { - Value::No => quote!(::uutils_args_complete::Value::No), - Value::Optional(name) => quote!(::uutils_args_complete::Value::Optional(#name)), - Value::Required(name) => quote!(::uutils_args_complete::Value::Required(#name)), + Value::No => quote!(::uutils_args::complete::Value::No), + Value::Optional(name) => { + quote!(::uutils_args::complete::Value::Optional(#name)) + } + Value::Required(name) => { + quote!(::uutils_args::complete::Value::Required(#name)) + } }; - quote!(::uutils_args_complete::Flag { + quote!(::uutils_args::complete::Flag { flag: #flag, value: #value }) @@ -81,7 +89,7 @@ pub fn complete(args: &[Argument], file: &Option) -> TokenStream { }; arg_specs.push(quote!( - ::uutils_args_complete::Arg { + ::uutils_args::complete::Arg { short: vec![#(#short),*], long: vec![#(#long),*], help: #help, @@ -90,7 +98,7 @@ pub fn complete(args: &[Argument], file: &Option) -> TokenStream { )) } - quote!(::uutils_args_complete::Command { + quote!(::uutils_args::complete::Command { name: option_env!("CARGO_BIN_NAME").unwrap_or(env!("CARGO_PKG_NAME")), summary: #summary, after_options: #after_options, diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 5061a3e..73a08a2 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -117,7 +117,7 @@ pub fn arguments(input: TokenStream) -> TokenStream { #version_string } - fn complete() -> ::uutils_args_complete::Command<'static> { + fn complete() -> ::uutils_args::complete::Command<'static> { use ::uutils_args::Value; #complete_command } @@ -212,9 +212,9 @@ pub fn value(input: TokenStream) -> TokenStream { }) } - fn value_hint() -> ::uutils_args_complete::ValueHint { + fn value_hint() -> ::uutils_args::complete::ValueHint { let keys: [&str; #keys_len] = [#(#all_keys),*]; - ::uutils_args_complete::ValueHint::Strings( + ::uutils_args::complete::ValueHint::Strings( keys .into_iter() .map(ToString::to_string) diff --git a/complete/src/bash.rs b/src/complete/bash.rs similarity index 97% rename from complete/src/bash.rs rename to src/complete/bash.rs index e72afad..d69eb8b 100644 --- a/complete/src/bash.rs +++ b/src/complete/bash.rs @@ -1,7 +1,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::{Command, Flag}; +use crate::complete::{Command, Flag}; /// Create completion script for `bash` /// @@ -35,7 +35,7 @@ pub fn render(c: &Command) -> String { #[cfg(test)] mod test { use super::render; - use crate::{Arg, Command, Flag, Value}; + use crate::complete::{Arg, Command, Flag, Value}; #[test] fn simple() { diff --git a/complete/src/fish.rs b/src/complete/fish.rs similarity index 97% rename from complete/src/fish.rs rename to src/complete/fish.rs index d6f0f66..d718e81 100644 --- a/complete/src/fish.rs +++ b/src/complete/fish.rs @@ -1,7 +1,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::{Command, Flag, ValueHint}; +use crate::complete::{Command, Flag, ValueHint}; /// Create completion script for `fish` /// @@ -45,7 +45,7 @@ fn render_value_hint(value: &ValueHint) -> String { #[cfg(test)] mod test { use super::render; - use crate::{Arg, Command, Flag, Value, ValueHint}; + use crate::complete::{Arg, Command, Flag, Value, ValueHint}; #[test] fn short() { diff --git a/complete/src/man.rs b/src/complete/man.rs similarity index 97% rename from complete/src/man.rs rename to src/complete/man.rs index 0e0a38c..d4befed 100644 --- a/complete/src/man.rs +++ b/src/complete/man.rs @@ -1,7 +1,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::{Command, Flag, Value}; +use crate::complete::{Command, Flag, Value}; use roff::{Roff, bold, italic, roman}; pub fn render(c: &Command) -> String { diff --git a/complete/src/md.rs b/src/complete/md.rs similarity index 97% rename from complete/src/md.rs rename to src/complete/md.rs index 1c2fa1b..883c5b5 100644 --- a/complete/src/md.rs +++ b/src/complete/md.rs @@ -1,7 +1,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::{Command, Flag, Value}; +use crate::complete::{Command, Flag, Value}; /// Render command to a markdown file for mdbook pub fn render(c: &Command) -> String { diff --git a/complete/src/lib.rs b/src/complete/mod.rs similarity index 100% rename from complete/src/lib.rs rename to src/complete/mod.rs diff --git a/complete/src/nu.rs b/src/complete/nu.rs similarity index 97% rename from complete/src/nu.rs rename to src/complete/nu.rs index fb9016c..e7f0271 100644 --- a/complete/src/nu.rs +++ b/src/complete/nu.rs @@ -1,7 +1,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::{Arg, Command, Flag, Value, ValueHint}; +use crate::complete::{Arg, Command, Flag, Value, ValueHint}; use std::fmt::Write; /// Create completion script for `nushell` diff --git a/complete/src/zsh.rs b/src/complete/zsh.rs similarity index 98% rename from complete/src/zsh.rs rename to src/complete/zsh.rs index 2b709f8..68f9d53 100644 --- a/complete/src/zsh.rs +++ b/src/complete/zsh.rs @@ -1,7 +1,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::{Arg, Command, Flag, Value, ValueHint}; +use crate::complete::{Arg, Command, Flag, Value, ValueHint}; /// Create completion script for `zsh` pub fn render(c: &Command) -> String { diff --git a/src/lib.rs b/src/lib.rs index e2c9cce..5a0be4f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,7 @@ //! #![doc = include_str!("../README.md")] +pub mod complete; mod error; pub mod internal; pub mod positional; @@ -103,7 +104,7 @@ pub trait Arguments: Sized { Ok(()) } - fn complete() -> uutils_args_complete::Command<'static>; + fn complete() -> complete::Command<'static>; } /// An iterator over arguments. @@ -197,7 +198,7 @@ pub trait Options: Sized { } fn complete(shell: &str) -> String { - uutils_args_complete::render(&Arg::complete(), shell) + complete::render(&Arg::complete(), shell) } } diff --git a/src/value.rs b/src/value.rs index 2109a6a..c3efc1d 100644 --- a/src/value.rs +++ b/src/value.rs @@ -1,12 +1,12 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +use crate::complete::ValueHint; use crate::error::{Error, ErrorKind}; use std::{ ffi::{OsStr, OsString}, path::PathBuf, }; -use uutils_args_complete::ValueHint; pub type ValueResult = Result>; @@ -95,7 +95,7 @@ where Ok(Some(T::from_value(value)?)) } - fn value_hint() -> uutils_args_complete::ValueHint { + fn value_hint() -> ValueHint { T::value_hint() } } From f47e84c7147a3b8cbdd351b4ad30e5911dfbcbad Mon Sep 17 00:00:00 2001 From: Ben Wiederhake Date: Tue, 15 Apr 2025 01:59:32 +0200 Subject: [PATCH 5/6] derive: split 'compile_error' toy into many test cases, demonstrate new issues --- Cargo.toml | 3 + examples/test_compile_errors_manually.rs | 84 ------------------- tests/derive.rs | 41 +++++++++ tests/derive/arg_bare_keyword.rs | 19 +++++ tests/derive/arg_bare_keyword.stderr | 16 ++++ tests/derive/arg_duplicate_other.rs | 21 +++++ tests/derive/arg_duplicate_within.rs | 19 +++++ tests/derive/arg_just_minus.rs | 19 +++++ tests/derive/arg_just_minus.stderr | 34 ++++++++ tests/derive/arg_key_value.rs | 19 +++++ tests/derive/arg_key_value.stderr | 34 ++++++++ tests/derive/arg_missing_closing_bracket.rs | 19 +++++ .../derive/arg_missing_closing_bracket.stderr | 34 ++++++++ tests/derive/arg_missing_equals.rs | 19 +++++ tests/derive/arg_missing_equals.stderr | 34 ++++++++ tests/derive/arg_missing_field.rs | 19 +++++ tests/derive/arg_missing_field.stderr | 34 ++++++++ tests/derive/arg_missing_metavar.rs | 23 +++++ tests/derive/arg_missing_metavar.stderr | 12 +++ tests/derive/arguments_file_isdir.rs | 17 ++++ tests/derive/arguments_file_isdir.stderr | 34 ++++++++ tests/derive/arguments_file_nonexistent.rs | 17 ++++ .../derive/arguments_file_nonexistent.stderr | 34 ++++++++ tests/derive/arguments_file_writeonly.rs | 17 ++++ tests/derive/arguments_file_writeonly.stderr | 34 ++++++++ tests/derive/value_bare_keyword.rs | 26 ++++++ tests/derive/value_bare_keyword.stderr | 30 +++++++ 27 files changed, 628 insertions(+), 84 deletions(-) delete mode 100644 examples/test_compile_errors_manually.rs create mode 100644 tests/derive.rs create mode 100644 tests/derive/arg_bare_keyword.rs create mode 100644 tests/derive/arg_bare_keyword.stderr create mode 100644 tests/derive/arg_duplicate_other.rs create mode 100644 tests/derive/arg_duplicate_within.rs create mode 100644 tests/derive/arg_just_minus.rs create mode 100644 tests/derive/arg_just_minus.stderr create mode 100644 tests/derive/arg_key_value.rs create mode 100644 tests/derive/arg_key_value.stderr create mode 100644 tests/derive/arg_missing_closing_bracket.rs create mode 100644 tests/derive/arg_missing_closing_bracket.stderr create mode 100644 tests/derive/arg_missing_equals.rs create mode 100644 tests/derive/arg_missing_equals.stderr create mode 100644 tests/derive/arg_missing_field.rs create mode 100644 tests/derive/arg_missing_field.stderr create mode 100644 tests/derive/arg_missing_metavar.rs create mode 100644 tests/derive/arg_missing_metavar.stderr create mode 100644 tests/derive/arguments_file_isdir.rs create mode 100644 tests/derive/arguments_file_isdir.stderr create mode 100644 tests/derive/arguments_file_nonexistent.rs create mode 100644 tests/derive/arguments_file_nonexistent.stderr create mode 100644 tests/derive/arguments_file_writeonly.rs create mode 100644 tests/derive/arguments_file_writeonly.stderr create mode 100644 tests/derive/value_bare_keyword.rs create mode 100644 tests/derive/value_bare_keyword.stderr diff --git a/Cargo.toml b/Cargo.toml index 6adaff6..89aedf9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,9 @@ roff = "0.2.1" strsim = "0.11.1" uutils-args-derive = { version = "0.1.0", path = "derive" } +[dev-dependencies] +trybuild = "1.0.104" + [features] parse-is-complete = [] diff --git a/examples/test_compile_errors_manually.rs b/examples/test_compile_errors_manually.rs deleted file mode 100644 index 7d08f10..0000000 --- a/examples/test_compile_errors_manually.rs +++ /dev/null @@ -1,84 +0,0 @@ -use uutils_args::{Arguments, Options, Value}; - -// Using a fully-fledged compile-error testsuite is a bit overkill, but we still -// want to make sure that the `derive` crate generates reasonable error messages. -// That's what this "example" is for. In the following, there are blocks of -// lines, one marked as POSITIVE and multiple lines marked as NEGATIVE. The -// committed version of this file should only contain POSITIVE. In order to run a -// test, comment out the POSITIVE line, and use a NEGATIVE line instead, and -// manually check whether you see a reasonable error message – ideally the error -// message indicated by the comment. One way to do this is: - -// $ cargo build --example test_compile_errors_manually - -#[derive(Value, Debug, Default)] -enum Flavor { - #[default] - #[value("kind", "nice")] - Kind, - #[value("condescending")] // POSITIVE - // #[value(condescending)] // NEGATIVE: "expected comma-separated list of string literals" - Condescending, -} - -#[derive(Arguments)] -#[arguments(file = "examples/hello_world_help.md")] // POSITIVE -// #[arguments(file = "examples/nonexistent.md")] // NEGATIVE: "cannot open help-string file" -// #[arguments(file = "/dev/full")] // NEGATIVE: Causes OOM, FIXME -// #[arguments(file = "/")] // NEGATIVE: "cannot read from help-string file" -// #[arguments(file = "path/to/some/WRITE-ONLY/file")] // NEGATIVE: "cannot open help-string file" -enum Arg { - /// The name to greet - #[arg("-n NAME", "--name[=NAME]", "name=NAME")] // POSITIVE - // #[arg("-")] // NEGATIVE: flag name must be non-empty (cannot be just '-') - // #[arg("-n NAME", "--name[NAME]", "name=NAME")] // NEGATIVE: "expected '=' after '[' in flag pattern" - // #[arg("-n NAME", "--name[=NAME", "name=NAME")] // NEGATIVE: "expected final ']' in flag pattern" - // #[arg(key="name")] // NEGATIVE: "can't parse arg attributes, expected one or more strings" - Name(String), - - /// The number of times to greet - #[arg("-c N", "--count=N")] - Count(u8), - - #[arg("--flavor=FLAVOR")] - Flavor(Flavor), -} - -struct Settings { - name: String, - count: u8, - flavor: Flavor, -} - -impl Options for Settings { - fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { - match arg { - Arg::Name(n) => self.name = n, - Arg::Count(c) => self.count = c, - Arg::Flavor(flavor) => self.flavor = flavor, - } - Ok(()) - } -} - -fn main() -> Result<(), uutils_args::Error> { - let (settings, _operands) = Settings { - name: String::new(), - count: 1, - flavor: Flavor::Kind, - } - .parse(std::env::args_os()) - .unwrap(); - - for _ in 0..settings.count { - match settings.flavor { - Flavor::Kind => { - println!("Hello, {}!", settings.name); - } - Flavor::Condescending => { - println!("Ugh, {}.", settings.name); - } - } - } - Ok(()) -} diff --git a/tests/derive.rs b/tests/derive.rs new file mode 100644 index 0000000..f3083d3 --- /dev/null +++ b/tests/derive.rs @@ -0,0 +1,41 @@ +#[test] +fn derive_error_messages_common() { + let t = trybuild::TestCases::new(); + t.compile_fail("tests/derive/arg_bare_keyword.rs"); + t.compile_fail("tests/derive/arg_just_minus.rs"); + t.compile_fail("tests/derive/arg_key_value.rs"); + t.compile_fail("tests/derive/arg_missing_closing_bracket.rs"); + t.compile_fail("tests/derive/arg_missing_equals.rs"); + t.compile_fail("tests/derive/arg_missing_field.rs"); + t.compile_fail("tests/derive/arg_missing_metavar.rs"); + t.compile_fail("tests/derive/arguments_file_nonexistent.rs"); + t.compile_fail("tests/derive/value_bare_keyword.rs"); + t.pass("tests/derive/arg_duplicate_other.rs"); // FIXME: Should fail! + t.pass("tests/derive/arg_duplicate_within.rs"); // FIXME: Should fail! +} + +#[cfg(unix)] +#[test] +fn derive_error_messages_unix() { + let t = trybuild::TestCases::new(); + t.compile_fail("tests/derive/arguments_file_isdir.rs"); // Needs the directory "/" +} + +#[cfg(target_os = "linux")] +#[test] +fn derive_error_messages_linux_writeonly_file() { + use std::fs::metadata; + use std::os::unix::fs::PermissionsExt; + + // First, verify that /proc/self/clear_refs exists and is write-only: + // https://man.archlinux.org/man/proc_pid_clear_refs.5.en + let metadata = metadata("/proc/self/clear_refs").expect("should be in Linux 2.6.22"); + eprintln!("is_file={}", metadata.is_file()); + eprintln!("permissions={:?}", metadata.permissions()); + assert_eq!(0o100200, metadata.permissions().mode()); + + // The file exists, as it should. Now we can run the test, using this + // special write-only file, without having to worry about clean-up: + let t = trybuild::TestCases::new(); + t.compile_fail("tests/derive/arguments_file_writeonly.rs"); +} diff --git a/tests/derive/arg_bare_keyword.rs b/tests/derive/arg_bare_keyword.rs new file mode 100644 index 0000000..96b2ef8 --- /dev/null +++ b/tests/derive/arg_bare_keyword.rs @@ -0,0 +1,19 @@ +use uutils_args::{Arguments, Options}; + +#[derive(Arguments)] +enum Arg { + #[arg(banana)] // Oops! + Something, +} + +struct Settings {} + +impl Options for Settings { + fn apply(&mut self, _arg: Arg) -> Result<(), uutils_args::Error> { + Ok(()) + } +} + +fn main() { + Settings {}.parse(std::env::args_os()).unwrap(); +} diff --git a/tests/derive/arg_bare_keyword.stderr b/tests/derive/arg_bare_keyword.stderr new file mode 100644 index 0000000..10a091c --- /dev/null +++ b/tests/derive/arg_bare_keyword.stderr @@ -0,0 +1,16 @@ +error[E0425]: cannot find function `banana` in this scope + --> tests/derive/arg_bare_keyword.rs:5:11 + | +5 | #[arg(banana)] // Oops! + | ^^^^^^ not found in this scope + +error[E0618]: expected function, found `Arg` + --> tests/derive/arg_bare_keyword.rs:3:10 + | +3 | #[derive(Arguments)] + | ^^^^^^^^^ call expression requires function +... +6 | Something, + | --------- `Arg::Something` defined here + | + = note: this error originates in the derive macro `Arguments` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/derive/arg_duplicate_other.rs b/tests/derive/arg_duplicate_other.rs new file mode 100644 index 0000000..f36607f --- /dev/null +++ b/tests/derive/arg_duplicate_other.rs @@ -0,0 +1,21 @@ +use uutils_args::{Arguments, Options}; + +#[derive(Arguments)] +enum Arg { + #[arg("--foo")] + Something, + #[arg("--foo")] // Oops! + Banana, +} + +struct Settings {} + +impl Options for Settings { + fn apply(&mut self, _arg: Arg) -> Result<(), uutils_args::Error> { + Ok(()) + } +} + +fn main() { + Settings {}.parse(std::env::args_os()).unwrap(); +} diff --git a/tests/derive/arg_duplicate_within.rs b/tests/derive/arg_duplicate_within.rs new file mode 100644 index 0000000..dd1ef30 --- /dev/null +++ b/tests/derive/arg_duplicate_within.rs @@ -0,0 +1,19 @@ +use uutils_args::{Arguments, Options}; + +#[derive(Arguments)] +enum Arg { + #[arg("--foo", "--foo")] // Oops! + Something, +} + +struct Settings {} + +impl Options for Settings { + fn apply(&mut self, _arg: Arg) -> Result<(), uutils_args::Error> { + Ok(()) + } +} + +fn main() { + Settings {}.parse(std::env::args_os()).unwrap(); +} diff --git a/tests/derive/arg_just_minus.rs b/tests/derive/arg_just_minus.rs new file mode 100644 index 0000000..b2d0e45 --- /dev/null +++ b/tests/derive/arg_just_minus.rs @@ -0,0 +1,19 @@ +use uutils_args::{Arguments, Options}; + +#[derive(Arguments)] +enum Arg { + #[arg("-")] // Oops! + Something, +} + +struct Settings {} + +impl Options for Settings { + fn apply(&mut self, _arg: Arg) -> Result<(), uutils_args::Error> { + Ok(()) + } +} + +fn main() { + Settings {}.parse(std::env::args_os()).unwrap(); +} diff --git a/tests/derive/arg_just_minus.stderr b/tests/derive/arg_just_minus.stderr new file mode 100644 index 0000000..7955bfa --- /dev/null +++ b/tests/derive/arg_just_minus.stderr @@ -0,0 +1,34 @@ +error: proc-macro derive panicked + --> tests/derive/arg_just_minus.rs:3:10 + | +3 | #[derive(Arguments)] + | ^^^^^^^^^ + | + = help: message: flag name must be non-empty (cannot be just '-') + +error[E0277]: the trait bound `Arg: uutils_args::Arguments` is not satisfied + --> tests/derive/arg_just_minus.rs:11:6 + | +11 | impl Options for Settings { + | ^^^^^^^^^^^^ the trait `uutils_args::Arguments` is not implemented for `Arg` + | +note: required by a bound in `Options` + --> src/lib.rs + | + | pub trait Options: Sized { + | ^^^^^^^^^ required by this bound in `Options` + +error[E0277]: the trait bound `Arg: uutils_args::Arguments` is not satisfied + --> tests/derive/arg_just_minus.rs:18:17 + | +18 | Settings {}.parse(std::env::args_os()).unwrap(); + | ^^^^^ the trait `uutils_args::Arguments` is not implemented for `Arg` + | +note: required by a bound in `uutils_args::Options::parse` + --> src/lib.rs + | + | pub trait Options: Sized { + | ^^^^^^^^^ required by this bound in `Options::parse` +... + | fn parse(mut self, args: I) -> Result<(Self, Vec), Error> + | ----- required by a bound in this associated function diff --git a/tests/derive/arg_key_value.rs b/tests/derive/arg_key_value.rs new file mode 100644 index 0000000..a525a59 --- /dev/null +++ b/tests/derive/arg_key_value.rs @@ -0,0 +1,19 @@ +use uutils_args::{Arguments, Options}; + +#[derive(Arguments)] +enum Arg { + #[arg(key = "name")] // Oops! + Something, +} + +struct Settings {} + +impl Options for Settings { + fn apply(&mut self, _arg: Arg) -> Result<(), uutils_args::Error> { + Ok(()) + } +} + +fn main() { + Settings {}.parse(std::env::args_os()).unwrap(); +} diff --git a/tests/derive/arg_key_value.stderr b/tests/derive/arg_key_value.stderr new file mode 100644 index 0000000..3a67462 --- /dev/null +++ b/tests/derive/arg_key_value.stderr @@ -0,0 +1,34 @@ +error: proc-macro derive panicked + --> tests/derive/arg_key_value.rs:3:10 + | +3 | #[derive(Arguments)] + | ^^^^^^^^^ + | + = help: message: can't parse arg attributes, expected one or more strings: Error("expected `,`") + +error[E0277]: the trait bound `Arg: uutils_args::Arguments` is not satisfied + --> tests/derive/arg_key_value.rs:11:6 + | +11 | impl Options for Settings { + | ^^^^^^^^^^^^ the trait `uutils_args::Arguments` is not implemented for `Arg` + | +note: required by a bound in `Options` + --> src/lib.rs + | + | pub trait Options: Sized { + | ^^^^^^^^^ required by this bound in `Options` + +error[E0277]: the trait bound `Arg: uutils_args::Arguments` is not satisfied + --> tests/derive/arg_key_value.rs:18:17 + | +18 | Settings {}.parse(std::env::args_os()).unwrap(); + | ^^^^^ the trait `uutils_args::Arguments` is not implemented for `Arg` + | +note: required by a bound in `uutils_args::Options::parse` + --> src/lib.rs + | + | pub trait Options: Sized { + | ^^^^^^^^^ required by this bound in `Options::parse` +... + | fn parse(mut self, args: I) -> Result<(Self, Vec), Error> + | ----- required by a bound in this associated function diff --git a/tests/derive/arg_missing_closing_bracket.rs b/tests/derive/arg_missing_closing_bracket.rs new file mode 100644 index 0000000..3edfba1 --- /dev/null +++ b/tests/derive/arg_missing_closing_bracket.rs @@ -0,0 +1,19 @@ +use uutils_args::{Arguments, Options}; + +#[derive(Arguments)] +enum Arg { + #[arg("--foo[=FOO")] // Oops! + Something(String), +} + +struct Settings {} + +impl Options for Settings { + fn apply(&mut self, _arg: Arg) -> Result<(), uutils_args::Error> { + Ok(()) + } +} + +fn main() { + Settings {}.parse(std::env::args_os()).unwrap(); +} diff --git a/tests/derive/arg_missing_closing_bracket.stderr b/tests/derive/arg_missing_closing_bracket.stderr new file mode 100644 index 0000000..4aec1fd --- /dev/null +++ b/tests/derive/arg_missing_closing_bracket.stderr @@ -0,0 +1,34 @@ +error: proc-macro derive panicked + --> tests/derive/arg_missing_closing_bracket.rs:3:10 + | +3 | #[derive(Arguments)] + | ^^^^^^^^^ + | + = help: message: expected final ']' in flag pattern + +error[E0277]: the trait bound `Arg: uutils_args::Arguments` is not satisfied + --> tests/derive/arg_missing_closing_bracket.rs:11:6 + | +11 | impl Options for Settings { + | ^^^^^^^^^^^^ the trait `uutils_args::Arguments` is not implemented for `Arg` + | +note: required by a bound in `Options` + --> src/lib.rs + | + | pub trait Options: Sized { + | ^^^^^^^^^ required by this bound in `Options` + +error[E0277]: the trait bound `Arg: uutils_args::Arguments` is not satisfied + --> tests/derive/arg_missing_closing_bracket.rs:18:17 + | +18 | Settings {}.parse(std::env::args_os()).unwrap(); + | ^^^^^ the trait `uutils_args::Arguments` is not implemented for `Arg` + | +note: required by a bound in `uutils_args::Options::parse` + --> src/lib.rs + | + | pub trait Options: Sized { + | ^^^^^^^^^ required by this bound in `Options::parse` +... + | fn parse(mut self, args: I) -> Result<(Self, Vec), Error> + | ----- required by a bound in this associated function diff --git a/tests/derive/arg_missing_equals.rs b/tests/derive/arg_missing_equals.rs new file mode 100644 index 0000000..f988a1a --- /dev/null +++ b/tests/derive/arg_missing_equals.rs @@ -0,0 +1,19 @@ +use uutils_args::{Arguments, Options}; + +#[derive(Arguments)] +enum Arg { + #[arg("--foo[FOO]")] // Oops! + Something(String), +} + +struct Settings {} + +impl Options for Settings { + fn apply(&mut self, _arg: Arg) -> Result<(), uutils_args::Error> { + Ok(()) + } +} + +fn main() { + Settings {}.parse(std::env::args_os()).unwrap(); +} diff --git a/tests/derive/arg_missing_equals.stderr b/tests/derive/arg_missing_equals.stderr new file mode 100644 index 0000000..e00b141 --- /dev/null +++ b/tests/derive/arg_missing_equals.stderr @@ -0,0 +1,34 @@ +error: proc-macro derive panicked + --> tests/derive/arg_missing_equals.rs:3:10 + | +3 | #[derive(Arguments)] + | ^^^^^^^^^ + | + = help: message: expected '=' after '[' in flag pattern + +error[E0277]: the trait bound `Arg: uutils_args::Arguments` is not satisfied + --> tests/derive/arg_missing_equals.rs:11:6 + | +11 | impl Options for Settings { + | ^^^^^^^^^^^^ the trait `uutils_args::Arguments` is not implemented for `Arg` + | +note: required by a bound in `Options` + --> src/lib.rs + | + | pub trait Options: Sized { + | ^^^^^^^^^ required by this bound in `Options` + +error[E0277]: the trait bound `Arg: uutils_args::Arguments` is not satisfied + --> tests/derive/arg_missing_equals.rs:18:17 + | +18 | Settings {}.parse(std::env::args_os()).unwrap(); + | ^^^^^ the trait `uutils_args::Arguments` is not implemented for `Arg` + | +note: required by a bound in `uutils_args::Options::parse` + --> src/lib.rs + | + | pub trait Options: Sized { + | ^^^^^^^^^ required by this bound in `Options::parse` +... + | fn parse(mut self, args: I) -> Result<(Self, Vec), Error> + | ----- required by a bound in this associated function diff --git a/tests/derive/arg_missing_field.rs b/tests/derive/arg_missing_field.rs new file mode 100644 index 0000000..88418ed --- /dev/null +++ b/tests/derive/arg_missing_field.rs @@ -0,0 +1,19 @@ +use uutils_args::{Arguments, Options}; + +#[derive(Arguments)] +enum Arg { + #[arg("--foo[=FOO]")] + Something, // Oops! +} + +struct Settings {} + +impl Options for Settings { + fn apply(&mut self, _arg: Arg) -> Result<(), uutils_args::Error> { + Ok(()) + } +} + +fn main() { + Settings {}.parse(std::env::args_os()).unwrap(); +} diff --git a/tests/derive/arg_missing_field.stderr b/tests/derive/arg_missing_field.stderr new file mode 100644 index 0000000..eff1a85 --- /dev/null +++ b/tests/derive/arg_missing_field.stderr @@ -0,0 +1,34 @@ +error: proc-macro derive panicked + --> tests/derive/arg_missing_field.rs:3:10 + | +3 | #[derive(Arguments)] + | ^^^^^^^^^ + | + = help: message: Option cannot take a value if the variant doesn't have a field + +error[E0277]: the trait bound `Arg: uutils_args::Arguments` is not satisfied + --> tests/derive/arg_missing_field.rs:11:6 + | +11 | impl Options for Settings { + | ^^^^^^^^^^^^ the trait `uutils_args::Arguments` is not implemented for `Arg` + | +note: required by a bound in `Options` + --> src/lib.rs + | + | pub trait Options: Sized { + | ^^^^^^^^^ required by this bound in `Options` + +error[E0277]: the trait bound `Arg: uutils_args::Arguments` is not satisfied + --> tests/derive/arg_missing_field.rs:18:17 + | +18 | Settings {}.parse(std::env::args_os()).unwrap(); + | ^^^^^ the trait `uutils_args::Arguments` is not implemented for `Arg` + | +note: required by a bound in `uutils_args::Options::parse` + --> src/lib.rs + | + | pub trait Options: Sized { + | ^^^^^^^^^ required by this bound in `Options::parse` +... + | fn parse(mut self, args: I) -> Result<(Self, Vec), Error> + | ----- required by a bound in this associated function diff --git a/tests/derive/arg_missing_metavar.rs b/tests/derive/arg_missing_metavar.rs new file mode 100644 index 0000000..cd68af7 --- /dev/null +++ b/tests/derive/arg_missing_metavar.rs @@ -0,0 +1,23 @@ +use uutils_args::{Arguments, Options}; + +struct Complicated { + // Doesn't "impl Default". Oops! +} + +#[derive(Arguments)] +enum Arg { + #[arg("--foo")] + Something(Complicated), +} + +struct Settings {} + +impl Options for Settings { + fn apply(&mut self, _arg: Arg) -> Result<(), uutils_args::Error> { + Ok(()) + } +} + +fn main() { + Settings {}.parse(std::env::args_os()).unwrap(); +} diff --git a/tests/derive/arg_missing_metavar.stderr b/tests/derive/arg_missing_metavar.stderr new file mode 100644 index 0000000..993ce06 --- /dev/null +++ b/tests/derive/arg_missing_metavar.stderr @@ -0,0 +1,12 @@ +error[E0277]: the trait bound `Complicated: Default` is not satisfied + --> tests/derive/arg_missing_metavar.rs:7:10 + | +7 | #[derive(Arguments)] + | ^^^^^^^^^ the trait `Default` is not implemented for `Complicated` + | + = note: this error originates in the derive macro `Arguments` (in Nightly builds, run with -Z macro-backtrace for more info) +help: consider annotating `Complicated` with `#[derive(Default)]` + | +3 + #[derive(Default)] +4 | struct Complicated { + | diff --git a/tests/derive/arguments_file_isdir.rs b/tests/derive/arguments_file_isdir.rs new file mode 100644 index 0000000..8df1342 --- /dev/null +++ b/tests/derive/arguments_file_isdir.rs @@ -0,0 +1,17 @@ +use uutils_args::{Arguments, Options}; + +#[derive(Arguments)] +#[arguments(file = "/")] // Oops! +enum Arg {} + +struct Settings {} + +impl Options for Settings { + fn apply(&mut self, _arg: Arg) -> Result<(), uutils_args::Error> { + Ok(()) + } +} + +fn main() { + Settings {}.parse(std::env::args_os()).unwrap(); +} diff --git a/tests/derive/arguments_file_isdir.stderr b/tests/derive/arguments_file_isdir.stderr new file mode 100644 index 0000000..944a429 --- /dev/null +++ b/tests/derive/arguments_file_isdir.stderr @@ -0,0 +1,34 @@ +error: proc-macro derive panicked + --> tests/derive/arguments_file_isdir.rs:3:10 + | +3 | #[derive(Arguments)] + | ^^^^^^^^^ + | + = help: message: cannot read from help-string file: Os { code: 21, kind: IsADirectory, message: "Is a directory" } + +error[E0277]: the trait bound `Arg: uutils_args::Arguments` is not satisfied + --> tests/derive/arguments_file_isdir.rs:9:6 + | +9 | impl Options for Settings { + | ^^^^^^^^^^^^ the trait `uutils_args::Arguments` is not implemented for `Arg` + | +note: required by a bound in `Options` + --> src/lib.rs + | + | pub trait Options: Sized { + | ^^^^^^^^^ required by this bound in `Options` + +error[E0277]: the trait bound `Arg: uutils_args::Arguments` is not satisfied + --> tests/derive/arguments_file_isdir.rs:16:17 + | +16 | Settings {}.parse(std::env::args_os()).unwrap(); + | ^^^^^ the trait `uutils_args::Arguments` is not implemented for `Arg` + | +note: required by a bound in `uutils_args::Options::parse` + --> src/lib.rs + | + | pub trait Options: Sized { + | ^^^^^^^^^ required by this bound in `Options::parse` +... + | fn parse(mut self, args: I) -> Result<(Self, Vec), Error> + | ----- required by a bound in this associated function diff --git a/tests/derive/arguments_file_nonexistent.rs b/tests/derive/arguments_file_nonexistent.rs new file mode 100644 index 0000000..bdabf54 --- /dev/null +++ b/tests/derive/arguments_file_nonexistent.rs @@ -0,0 +1,17 @@ +use uutils_args::{Arguments, Options}; + +#[derive(Arguments)] +#[arguments(file = "nonexistent")] // Oops! +enum Arg {} + +struct Settings {} + +impl Options for Settings { + fn apply(&mut self, _arg: Arg) -> Result<(), uutils_args::Error> { + Ok(()) + } +} + +fn main() { + Settings {}.parse(std::env::args_os()).unwrap(); +} diff --git a/tests/derive/arguments_file_nonexistent.stderr b/tests/derive/arguments_file_nonexistent.stderr new file mode 100644 index 0000000..8ea6b22 --- /dev/null +++ b/tests/derive/arguments_file_nonexistent.stderr @@ -0,0 +1,34 @@ +error: proc-macro derive panicked + --> tests/derive/arguments_file_nonexistent.rs:3:10 + | +3 | #[derive(Arguments)] + | ^^^^^^^^^ + | + = help: message: cannot open help-string file: Os { code: 2, kind: NotFound, message: "No such file or directory" } + +error[E0277]: the trait bound `Arg: uutils_args::Arguments` is not satisfied + --> tests/derive/arguments_file_nonexistent.rs:9:6 + | +9 | impl Options for Settings { + | ^^^^^^^^^^^^ the trait `uutils_args::Arguments` is not implemented for `Arg` + | +note: required by a bound in `Options` + --> src/lib.rs + | + | pub trait Options: Sized { + | ^^^^^^^^^ required by this bound in `Options` + +error[E0277]: the trait bound `Arg: uutils_args::Arguments` is not satisfied + --> tests/derive/arguments_file_nonexistent.rs:16:17 + | +16 | Settings {}.parse(std::env::args_os()).unwrap(); + | ^^^^^ the trait `uutils_args::Arguments` is not implemented for `Arg` + | +note: required by a bound in `uutils_args::Options::parse` + --> src/lib.rs + | + | pub trait Options: Sized { + | ^^^^^^^^^ required by this bound in `Options::parse` +... + | fn parse(mut self, args: I) -> Result<(Self, Vec), Error> + | ----- required by a bound in this associated function diff --git a/tests/derive/arguments_file_writeonly.rs b/tests/derive/arguments_file_writeonly.rs new file mode 100644 index 0000000..f598635 --- /dev/null +++ b/tests/derive/arguments_file_writeonly.rs @@ -0,0 +1,17 @@ +use uutils_args::{Arguments, Options}; + +#[derive(Arguments)] +#[arguments(file = "/proc/self/clear_refs")] // Oops! +enum Arg {} + +struct Settings {} + +impl Options for Settings { + fn apply(&mut self, _arg: Arg) -> Result<(), uutils_args::Error> { + Ok(()) + } +} + +fn main() { + Settings {}.parse(std::env::args_os()).unwrap(); +} diff --git a/tests/derive/arguments_file_writeonly.stderr b/tests/derive/arguments_file_writeonly.stderr new file mode 100644 index 0000000..403bd7c --- /dev/null +++ b/tests/derive/arguments_file_writeonly.stderr @@ -0,0 +1,34 @@ +error: proc-macro derive panicked + --> tests/derive/arguments_file_writeonly.rs:3:10 + | +3 | #[derive(Arguments)] + | ^^^^^^^^^ + | + = help: message: cannot open help-string file: Os { code: 13, kind: PermissionDenied, message: "Permission denied" } + +error[E0277]: the trait bound `Arg: uutils_args::Arguments` is not satisfied + --> tests/derive/arguments_file_writeonly.rs:9:6 + | +9 | impl Options for Settings { + | ^^^^^^^^^^^^ the trait `uutils_args::Arguments` is not implemented for `Arg` + | +note: required by a bound in `Options` + --> src/lib.rs + | + | pub trait Options: Sized { + | ^^^^^^^^^ required by this bound in `Options` + +error[E0277]: the trait bound `Arg: uutils_args::Arguments` is not satisfied + --> tests/derive/arguments_file_writeonly.rs:16:17 + | +16 | Settings {}.parse(std::env::args_os()).unwrap(); + | ^^^^^ the trait `uutils_args::Arguments` is not implemented for `Arg` + | +note: required by a bound in `uutils_args::Options::parse` + --> src/lib.rs + | + | pub trait Options: Sized { + | ^^^^^^^^^ required by this bound in `Options::parse` +... + | fn parse(mut self, args: I) -> Result<(Self, Vec), Error> + | ----- required by a bound in this associated function diff --git a/tests/derive/value_bare_keyword.rs b/tests/derive/value_bare_keyword.rs new file mode 100644 index 0000000..7214066 --- /dev/null +++ b/tests/derive/value_bare_keyword.rs @@ -0,0 +1,26 @@ +use uutils_args::{Arguments, Options, Value}; + +#[derive(Value, Default)] +enum Flavor { + #[default] + #[value(banana)] // Oops! + Banana, +} + +#[derive(Arguments)] +enum Arg { + #[arg("--flavor=FLAVOR")] + Flavor(Flavor), +} + +struct Settings {} + +impl Options for Settings { + fn apply(&mut self, _arg: Arg) -> Result<(), uutils_args::Error> { + Ok(()) + } +} + +fn main() { + Settings {}.parse(std::env::args_os()).unwrap(); +} diff --git a/tests/derive/value_bare_keyword.stderr b/tests/derive/value_bare_keyword.stderr new file mode 100644 index 0000000..1fdb7e8 --- /dev/null +++ b/tests/derive/value_bare_keyword.stderr @@ -0,0 +1,30 @@ +error: proc-macro derive panicked + --> tests/derive/value_bare_keyword.rs:3:10 + | +3 | #[derive(Value, Default)] + | ^^^^^ + | + = help: message: expected comma-separated list of string literals: Error("unexpected end of input, unrecognized keyword in value attribute") + +error[E0277]: the trait bound `Flavor: uutils_args::Value` is not satisfied + --> tests/derive/value_bare_keyword.rs:10:10 + | +10 | #[derive(Arguments)] + | ^^^^^^^^^ the trait `uutils_args::Value` is not implemented for `Flavor` + | + = help: the following other types implement trait `uutils_args::Value`: + Option + OsString + PathBuf + String + i128 + i16 + i32 + i64 + and $N others +note: required by a bound in `parse_value_for_option` + --> src/internal.rs + | + | pub fn parse_value_for_option(opt: &str, v: &OsStr) -> Result { + | ^^^^^ required by this bound in `parse_value_for_option` + = note: this error originates in the derive macro `Arguments` (in Nightly builds, run with -Z macro-backtrace for more info) From 0c4136fb600252c92cd9da0fc3bd4e41ddffb4f5 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Fri, 16 May 2025 10:49:41 +0200 Subject: [PATCH 6/6] clippy: suppress warning from large_enum_variant --- derive/src/attributes.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/derive/src/attributes.rs b/derive/src/attributes.rs index da582f8..8604b1e 100644 --- a/derive/src/attributes.rs +++ b/derive/src/attributes.rs @@ -69,6 +69,7 @@ impl ArgumentsAttr { } } +#[allow(clippy::large_enum_variant)] pub enum ArgAttr { Option(OptionAttr), Free(FreeAttr),