From 781a49ed2f6d04c9b67af722dad6a3b395071826 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 30 Nov 2023 10:03:16 +0000 Subject: [PATCH 001/116] Add renovate.json --- renovate.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 renovate.json diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..5db72dd --- /dev/null +++ b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended" + ] +} From d3ec7cd308690e308f8a6ee767cfc434472958f1 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Thu, 30 Nov 2023 11:20:20 +0100 Subject: [PATCH 002/116] add license headers and LICENSE file for derive --- LICENSE | 2 +- derive/LICENSE | 1 + derive/src/argument.rs | 3 +++ derive/src/attributes.rs | 3 +++ derive/src/flags.rs | 3 +++ derive/src/help.rs | 3 +++ derive/src/help_parser.rs | 2 -- derive/src/initial.rs | 3 +++ derive/src/lib.rs | 3 +++ src/error.rs | 3 +++ src/lib.rs | 3 +++ src/value.rs | 3 +++ 12 files changed, 29 insertions(+), 3 deletions(-) create mode 120000 derive/LICENSE diff --git a/LICENSE b/LICENSE index db1aed9..21bd444 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) Terts Diepraam +Copyright (c) uutils developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/derive/LICENSE b/derive/LICENSE new file mode 120000 index 0000000..ea5b606 --- /dev/null +++ b/derive/LICENSE @@ -0,0 +1 @@ +../LICENSE \ No newline at end of file diff --git a/derive/src/argument.rs b/derive/src/argument.rs index 3e14da7..3854be5 100644 --- a/derive/src/argument.rs +++ b/derive/src/argument.rs @@ -1,3 +1,6 @@ +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + use std::ops::RangeInclusive; use proc_macro2::TokenStream; diff --git a/derive/src/attributes.rs b/derive/src/attributes.rs index 2a19604..e8c915c 100644 --- a/derive/src/attributes.rs +++ b/derive/src/attributes.rs @@ -1,3 +1,6 @@ +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + use std::ops::RangeInclusive; use syn::{ diff --git a/derive/src/flags.rs b/derive/src/flags.rs index b999f0a..7cab9e5 100644 --- a/derive/src/flags.rs +++ b/derive/src/flags.rs @@ -1,3 +1,6 @@ +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + use proc_macro2::TokenStream; use quote::quote; diff --git a/derive/src/help.rs b/derive/src/help.rs index a853b77..8348ad0 100644 --- a/derive/src/help.rs +++ b/derive/src/help.rs @@ -1,3 +1,6 @@ +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + use std::{ io::Read, path::{Path, PathBuf}, diff --git a/derive/src/help_parser.rs b/derive/src/help_parser.rs index 8faa4e6..3d9e5ae 100644 --- a/derive/src/help_parser.rs +++ b/derive/src/help_parser.rs @@ -1,5 +1,3 @@ -// This file is part of the uutils coreutils package. -// // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. diff --git a/derive/src/initial.rs b/derive/src/initial.rs index 1cb0e83..ced4032 100644 --- a/derive/src/initial.rs +++ b/derive/src/initial.rs @@ -1,3 +1,6 @@ +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + use syn::{ parse::{Parse, ParseStream}, parse_macro_input, Data, DeriveInput, Fields, Token, diff --git a/derive/src/lib.rs b/derive/src/lib.rs index e78c358..8c3d4cc 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -1,3 +1,6 @@ +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + mod argument; mod attributes; mod flags; diff --git a/src/error.rs b/src/error.rs index 9c6c2a0..e2c9faa 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,6 @@ +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + use std::{ error::Error as StdError, ffi::OsString, diff --git a/src/lib.rs b/src/lib.rs index 80a95df..9a86a0a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,6 @@ +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + //! Argument parsing for the uutils coreutils project //! //! This crate provides the argument parsing for the diff --git a/src/value.rs b/src/value.rs index e891d0b..e95b9a4 100644 --- a/src/value.rs +++ b/src/value.rs @@ -1,3 +1,6 @@ +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + use crate::error::Error; use std::{ ffi::{OsStr, OsString}, From 657474ea3ca0be23829d5a1b76e3d707dd034d66 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Thu, 30 Nov 2023 11:22:26 +0100 Subject: [PATCH 003/116] 'derive' -> 'uutils-args-derive' --- Cargo.toml | 2 +- derive/Cargo.toml | 2 +- src/lib.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3f5b885..5098f13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ repository = "https://github.com/tertsdiepraam/uutils-args" readme = "README.md" [dependencies] -derive = { version = "0.1.0", path = "derive" } +uutils-args-derive = { version = "0.1.0", path = "derive" } strsim = "0.10.0" lexopt = "0.3.0" diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 2b73b0d..f98d1a8 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "derive" +name = "uutils-args-derive" version = "0.1.0" edition = "2021" diff --git a/src/lib.rs b/src/lib.rs index 9a86a0a..8f8a729 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -144,8 +144,8 @@ mod error; mod value; -pub use derive::*; pub use lexopt; +pub use uutils_args_derive::*; pub use error::Error; pub use value::{Value, ValueError, ValueResult}; From 89013f142bb7ccea46b8a55726f82674f1543586 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Thu, 30 Nov 2023 11:31:33 +0100 Subject: [PATCH 004/116] move module documentation to README --- README.md | 138 +++++++++++++++++++++++++++++++++++++++++++++++++- src/lib.rs | 144 ++--------------------------------------------------- 2 files changed, 140 insertions(+), 142 deletions(-) diff --git a/README.md b/README.md index 2f71c24..c07c5d8 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,139 @@ # uutils-args -An experimental derive-based argument parser specifically for uutils +Argument parsing for the [uutils coreutils](https://www.github.com/uutils/coreutils) project. + +It is designed to be flexible, while providing default +behaviour that aligns with GNU coreutils. + +## Features + + - A derive macro for declarative argument definition. + - Automatic help generation. + - Positional and optional arguments. + - Automatically parsing values into Rust types. + - Define a custom exit code on errors. + - Automatically accept unambiguous abbreviations of long options. + - Handles invalid UTF-8 gracefully. + +## When you should not use this library + +The goal of this library is to make it easy to build applications that +mimic the behaviour of the GNU coreutils. There are other applications +that have similar behaviour, which are C application that use `getopt` +and `getopt_long`. If you want to mimic that behaviour exactly, this +is the library for you. If you want to write basically anything else, +you should probably pick another argument parser. + +## Getting Started + +Parsing with this library consists of two "phases". In the first +phase, the arguments are mapped to an iterator of an `enum` +implementing [`Arguments`]. The second phase is mapping these +arguments onto a `struct` implementing [`Options`]. By defining +your arguments this way, there is a clear divide between the public +API and the internal representation of the settings of your app. + +For more information on these traits, see their respective documentation: + +- [`Arguments`] +- [`Options`] + +Below is a minimal example of a full CLI application using this library. + +```rust +use uutils_args::{Arguments, Initial, Options}; + +#[derive(Arguments)] +enum Arg { + // The doc strings below will be part of the `--help` text + // First we define a simple flag: + /// Do not transform input text to uppercase + #[option("-n", "--no-caps")] + NoCaps, + + // This option takes a value: + /// Add exclamation marks to output + #[option("-e N", "--exclaim=N")] + ExclamationMarks(u8), + + // This is a positional argument, the range specifies that + // at least one positional argument must be passed. + #[positional(1..)] + Text(String), +} + +#[derive(Initial)] +struct Settings { + // We can change the default value with the field attribute. + #[initial(true)] + caps: bool, + exclamation_marks: u8, + text: String, +} + +// To implement `Options`, we only need to provide the `apply` method. +// The `parse` method will be automatically generated. +impl Options for Settings { + fn apply(&mut self, arg: Arg) { + match arg { + Arg::NoCaps => self.caps = false, + Arg::ExclamationMarks(n) => self.exclamation_marks += n, + Arg::Text(s) => { + if self.text.is_empty() { + self.text.push_str(&s); + } else { + self.text.push(' '); + self.text.push_str(&s); + } + } + } + } +} + +fn run(args: &'static [&'static str]) -> String { + let s = Settings::parse(args); + let mut output = if s.caps { + s.text.to_uppercase() + } else { + s.text + }; + for i in 0..s.exclamation_marks { + output.push('!'); + } + output +} + +// The first argument is the binary name. In this example it's ignored. +assert_eq!(run(&["shout", "hello"]), "HELLO"); +assert_eq!(run(&["shout", "-e3", "hello"]), "HELLO!!!"); +assert_eq!(run(&["shout", "-e", "3", "hello"]), "HELLO!!!"); +assert_eq!(run(&["shout", "--no-caps", "hello"]), "hello"); +assert_eq!(run(&["shout", "-e3", "-n", "hello"]), "hello!!!"); +assert_eq!(run(&["shout", "-e3", "hello", "world"]), "HELLO WORLD!!!"); +``` + +## Additional functionality + +To make it easier to implement [`Arguments`] and [`Options`], there are +two additional traits: + +- [`Initial`] is an alternative to the [`Default`] trait from the standard + library, with a richer derive macro. +- [`Value`] allows for easy parsing from `OsStr` to any type + implementing [`Value`]. This crate also provides a derive macro for + this trait. + +## Examples + +The following files contain examples of commands defined with +`uutils_args`: + +- [hello world](https://github.com/tertsdiepraam/uutils-args/blob/main/examples/hello_world.rs) +- [arch](https://github.com/tertsdiepraam/uutils-args/blob/main/tests/coreutils/arch.rs) +- [b2sum](https://github.com/tertsdiepraam/uutils-args/blob/main/tests/coreutils/b2sum.rs) +- [base32](https://github.com/tertsdiepraam/uutils-args/blob/main/tests/coreutils/base32.rs) +- [basename](https://github.com/tertsdiepraam/uutils-args/blob/main/tests/coreutils/basename.rs) +- [cat](https://github.com/tertsdiepraam/uutils-args/blob/main/tests/coreutils/cat.rs) +- [echo](https://github.com/tertsdiepraam/uutils-args/blob/main/tests/coreutils/echo.rs) +- [ls](https://github.com/tertsdiepraam/uutils-args/blob/main/tests/coreutils/ls.rs) +- [mktemp](https://github.com/tertsdiepraam/uutils-args/blob/main/tests/coreutils/mktemp.rs) diff --git a/src/lib.rs b/src/lib.rs index 8f8a729..72321c8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,145 +1,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -//! Argument parsing for the uutils coreutils project -//! -//! This crate provides the argument parsing for the -//! [uutils coreutils](https://www.github.com/uutils/coreutils) -//! It is designed to be flexible, while providing default -//! behaviour that aligns with GNU coreutils. -//! -//! # Features -//! -//! - A derive macro for declarative argument definition. -//! - Automatic help generation. -//! - Positional and optional arguments. -//! - Automatically parsing values into Rust types. -//! - Define a custom exit code on errors. -//! - Automatically accept unambiguous abbreviations of long options. -//! - Handles invalid UTF-8 gracefully. -//! -//! # When you should not use this library -//! -//! The goal of this library is to make it easy to build applications that -//! mimic the behaviour of the GNU coreutils. There are other applications -//! that have similar behaviour, which are C application that use `getopt` -//! and `getopt_long`. If you want to mimic that behaviour exactly, this -//! is the library for you. If you want to write basically anything else, -//! you should probably pick another argument parser. -//! -//! # Getting Started -//! -//! Parsing with this library consists of two "phases". In the first -//! phase, the arguments are mapped to an iterator of an `enum` -//! implementing [`Arguments`]. The second phase is mapping these -//! arguments onto a `struct` implementing [`Options`]. By defining -//! your arguments this way, there is a clear divide between the public -//! API and the internal representation of the settings of your app. -//! -//! For more information on these traits, see their respective documentation: -//! -//! - [`Arguments`] -//! - [`Options`] -//! -//! Below is a minimal example of a full CLI application using this library. -//! -//! ``` -//! use uutils_args::{Arguments, Initial, Options}; -//! -//! #[derive(Arguments)] -//! enum Arg { -//! // The doc strings below will be part of the `--help` text -//! // First we define a simple flag: -//! /// Do not transform input text to uppercase -//! #[option("-n", "--no-caps")] -//! NoCaps, -//! -//! // This option takes a value: -//! /// Add exclamation marks to output -//! #[option("-e N", "--exclaim=N")] -//! ExclamationMarks(u8), -//! -//! // This is a positional argument, the range specifies that -//! // at least one positional argument must be passed. -//! #[positional(1..)] -//! Text(String), -//! } -//! -//! #[derive(Initial)] -//! struct Settings { -//! // We can change the default value with the field attribute. -//! #[initial(true)] -//! caps: bool, -//! exclamation_marks: u8, -//! text: String, -//! } -//! -//! // To implement `Options`, we only need to provide the `apply` method. -//! // The `parse` method will be automatically generated. -//! impl Options for Settings { -//! fn apply(&mut self, arg: Arg) { -//! match arg { -//! Arg::NoCaps => self.caps = false, -//! Arg::ExclamationMarks(n) => self.exclamation_marks += n, -//! Arg::Text(s) => { -//! if self.text.is_empty() { -//! self.text.push_str(&s); -//! } else { -//! self.text.push(' '); -//! self.text.push_str(&s); -//! } -//! } -//! } -//! } -//! } -//! -//! fn run(args: &'static [&'static str]) -> String { -//! let s = Settings::parse(args); -//! let mut output = if s.caps { -//! s.text.to_uppercase() -//! } else { -//! s.text -//! }; -//! for i in 0..s.exclamation_marks { -//! output.push('!'); -//! } -//! output -//! } -//! -//! // The first argument is the binary name. In this example it's ignored. -//! assert_eq!(run(&["shout", "hello"]), "HELLO"); -//! assert_eq!(run(&["shout", "-e3", "hello"]), "HELLO!!!"); -//! assert_eq!(run(&["shout", "-e", "3", "hello"]), "HELLO!!!"); -//! assert_eq!(run(&["shout", "--no-caps", "hello"]), "hello"); -//! assert_eq!(run(&["shout", "-e3", "-n", "hello"]), "hello!!!"); -//! assert_eq!(run(&["shout", "-e3", "hello", "world"]), "HELLO WORLD!!!"); -//! ``` -//! -//! # Additional functionality -//! -//! To make it easier to implement [`Arguments`] and [`Options`], there are -//! two additional traits: -//! -//! - [`Initial`] is an alternative to the [`Default`] trait from the standard -//! library, with a richer derive macro. -//! - [`Value`] allows for easy parsing from `OsStr` to any type -//! implementing [`Value`]. This crate also provides a derive macro for -//! this trait. -//! -//! # Examples -//! -//! The following files contain examples of commands defined with -//! `uutils_args`: -//! -//! - [hello world](https://github.com/tertsdiepraam/uutils-args/blob/main/examples/hello_world.rs) -//! - [arch](https://github.com/tertsdiepraam/uutils-args/blob/main/tests/coreutils/arch.rs) -//! - [b2sum](https://github.com/tertsdiepraam/uutils-args/blob/main/tests/coreutils/b2sum.rs) -//! - [base32](https://github.com/tertsdiepraam/uutils-args/blob/main/tests/coreutils/base32.rs) -//! - [basename](https://github.com/tertsdiepraam/uutils-args/blob/main/tests/coreutils/basename.rs) -//! - [cat](https://github.com/tertsdiepraam/uutils-args/blob/main/tests/coreutils/cat.rs) -//! - [echo](https://github.com/tertsdiepraam/uutils-args/blob/main/tests/coreutils/echo.rs) -//! - [ls](https://github.com/tertsdiepraam/uutils-args/blob/main/tests/coreutils/ls.rs) -//! - [mktemp](https://github.com/tertsdiepraam/uutils-args/blob/main/tests/coreutils/mktemp.rs) +#![doc = include_str!("../README.md")] mod error; mod value; @@ -180,7 +42,7 @@ fn exit_if_err(res: Result, exit_code: i32) -> T { /// meaning that we can parse the individual arguments to `T`.\ /// /// Usually, this trait will be implemented via the -/// [derive macro](derive::Arguments) and does not need to be implemented +/// [derive macro](derive@Arguments) and does not need to be implemented /// manually. pub trait Arguments: Sized { /// The exit code to exit the program with on error. @@ -304,7 +166,7 @@ impl ArgumentIter { /// The `Initial` trait is used by `Options` to construct the initial /// state of the options before any arguments are parsed. /// -/// The [derive macro](derive::Initial) supports setting the initial +/// The [derive macro](derive@Initial) supports setting the initial /// value per field and parsing the initial values from environment /// variables. Otherwise, it will be equivalent to the derive macro /// for the [`Default`] trait. From 0f6baea5b48ccaa0dc3a4229f453172f57724e35 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Thu, 30 Nov 2023 12:54:18 +0100 Subject: [PATCH 005/116] remove pulldown-cmark dependency --- derive/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index f98d1a8..fba8054 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -10,6 +10,5 @@ proc_macro = true [dependencies] proc-macro2 = "1.0.47" -pulldown-cmark = "0.9.2" quote = "1.0.21" syn = { version = "2.0.18 ", features = ["full"] } From 1dbceb414e306138ea93e7de1318d5e1e2c7d904 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 1 Dec 2023 02:38:26 +0000 Subject: [PATCH 006/116] Update Rust crate syn to 2.0.39 --- derive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 2b73b0d..9677f5d 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -12,4 +12,4 @@ proc_macro = true proc-macro2 = "1.0.47" pulldown-cmark = "0.9.2" quote = "1.0.21" -syn = { version = "2.0.18 ", features = ["full"] } +syn = { version = "2.0.39", features = ["full"] } From 72f75e821823077900ba32e4c1e368b5fe724c7d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 2 Dec 2023 11:28:15 +0000 Subject: [PATCH 007/116] Update actions/checkout action to v4 --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da3847e..402939f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Rust toolchain uses: actions-rs/toolchain@v1 with: @@ -34,7 +34,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Rust toolchain uses: actions-rs/toolchain@v1 with: @@ -54,7 +54,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Rust toolchain uses: actions-rs/toolchain@v1 with: @@ -74,7 +74,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Rust toolchain uses: actions-rs/toolchain@v1 with: From aaf964a99afcb82108690920ca11c24b57b55460 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Sat, 2 Dec 2023 16:01:20 +0100 Subject: [PATCH 008/116] Remove dependabot.yml --- .github/dependabot.yml | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 4bdc7e3..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,12 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "cargo" - directory: "/" - schedule: - interval: "daily" - open-pull-requests-limit: 10 - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: daily - open-pull-requests-limit: 5 From 4b292a35442c61835d377366755ace05edd18bdd Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Sat, 2 Dec 2023 16:23:44 +0100 Subject: [PATCH 009/116] ci: use dtolnay/rust-toolchain instead of action-rs/toolchain to get rid of some CI warnings --- .github/workflows/ci.yml | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 402939f..3039939 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,11 +18,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - profile: minimal - override: true + uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - uses: actions-rs/cargo@v1 with: @@ -36,11 +32,8 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@stable with: - toolchain: stable - profile: minimal - override: true components: rustfmt - uses: Swatinem/rust-cache@v2 - name: Check formatting @@ -56,11 +49,8 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@stable with: - toolchain: stable - profile: minimal - override: true components: clippy - uses: Swatinem/rust-cache@v2 - name: Clippy check @@ -76,11 +66,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - profile: minimal - override: true + uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - name: Check documentation env: From 2e814ac872741a8918418078c2d3914aa22d2ae3 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Sat, 2 Dec 2023 17:10:59 +0100 Subject: [PATCH 010/116] fix links after changing repository ownership --- Cargo.toml | 4 ++-- README.md | 18 +++++++++--------- design/problems_with_clap.md | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5098f13..415b33a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,8 +5,8 @@ edition = "2021" authors = ["Terts Diepraam"] license = "MIT" -homepage = "https://github.com/tertsdiepraam/uutils-args" -repository = "https://github.com/tertsdiepraam/uutils-args" +homepage = "https://github.com/uutils/uutils-args" +repository = "https://github.com/uutils/uutils-args" readme = "README.md" [dependencies] diff --git a/README.md b/README.md index c07c5d8..0cfcbb2 100644 --- a/README.md +++ b/README.md @@ -128,12 +128,12 @@ two additional traits: The following files contain examples of commands defined with `uutils_args`: -- [hello world](https://github.com/tertsdiepraam/uutils-args/blob/main/examples/hello_world.rs) -- [arch](https://github.com/tertsdiepraam/uutils-args/blob/main/tests/coreutils/arch.rs) -- [b2sum](https://github.com/tertsdiepraam/uutils-args/blob/main/tests/coreutils/b2sum.rs) -- [base32](https://github.com/tertsdiepraam/uutils-args/blob/main/tests/coreutils/base32.rs) -- [basename](https://github.com/tertsdiepraam/uutils-args/blob/main/tests/coreutils/basename.rs) -- [cat](https://github.com/tertsdiepraam/uutils-args/blob/main/tests/coreutils/cat.rs) -- [echo](https://github.com/tertsdiepraam/uutils-args/blob/main/tests/coreutils/echo.rs) -- [ls](https://github.com/tertsdiepraam/uutils-args/blob/main/tests/coreutils/ls.rs) -- [mktemp](https://github.com/tertsdiepraam/uutils-args/blob/main/tests/coreutils/mktemp.rs) +- [hello world](https://github.com/uutils/uutils-args/blob/main/examples/hello_world.rs) +- [arch](https://github.com/uutils/uutils-args/blob/main/tests/coreutils/arch.rs) +- [b2sum](https://github.com/uutils/uutils-args/blob/main/tests/coreutils/b2sum.rs) +- [base32](https://github.com/uutils/uutils-args/blob/main/tests/coreutils/base32.rs) +- [basename](https://github.com/uutils/uutils-args/blob/main/tests/coreutils/basename.rs) +- [cat](https://github.com/uutils/uutils-args/blob/main/tests/coreutils/cat.rs) +- [echo](https://github.com/uutils/uutils-args/blob/main/tests/coreutils/echo.rs) +- [ls](https://github.com/uutils/uutils-args/blob/main/tests/coreutils/ls.rs) +- [mktemp](https://github.com/uutils/uutils-args/blob/main/tests/coreutils/mktemp.rs) diff --git a/design/problems_with_clap.md b/design/problems_with_clap.md index 9293984..689165c 100644 --- a/design/problems_with_clap.md +++ b/design/problems_with_clap.md @@ -185,7 +185,7 @@ libraries. - Extremely flexible, even supports `dd`-style. - A different configuration between short and long options requires a workaround. - A many-to-many relation ship is possible, though not very ergonomic. - - For more information, see: https://github.com/tertsdiepraam/uutils-args/issues/17 + - For more information, see: https://github.com/uutils/uutils-args/issues/17 - [`gumdrop`](https://github.com/murarth/gumdrop) - Does not handle invalid UTF-8. - Not configurable enough. From 1ecaf1a4decc989872574c7a6f7dd0401306b649 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 2 Dec 2023 16:51:07 +0000 Subject: [PATCH 011/116] fix(deps): update rust crate proc-macro2 to 1.0.70 --- derive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index fba8054..a0e3880 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -9,6 +9,6 @@ edition = "2021" proc_macro = true [dependencies] -proc-macro2 = "1.0.47" +proc-macro2 = "1.0.70" quote = "1.0.21" syn = { version = "2.0.18 ", features = ["full"] } From e6a7f379bfcc2d2f481741c28641d34de018ff7b Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Thu, 30 Nov 2023 12:40:16 +0100 Subject: [PATCH 012/116] unify the option/positional/free attributes into arg --- README.md | 8 +- derive/src/argument.rs | 34 ++--- derive/src/attributes.rs | 285 +++++++++++++++++++----------------- derive/src/flags.rs | 20 +-- derive/src/help.rs | 6 +- derive/src/lib.rs | 2 +- examples/deprecated.rs | 4 +- examples/hello_world.rs | 6 +- tests/coreutils/b2sum.rs | 18 +-- tests/coreutils/base32.rs | 8 +- tests/coreutils/basename.rs | 8 +- tests/coreutils/cat.rs | 20 +-- tests/coreutils/dd.rs | 26 ++-- tests/coreutils/echo.rs | 8 +- tests/coreutils/head.rs | 12 +- tests/coreutils/ls.rs | 106 +++++++------- tests/coreutils/mktemp.rs | 14 +- tests/coreutils/tail.rs | 30 ++-- tests/defaults.rs | 4 +- tests/exit_code.rs | 2 +- tests/flags.rs | 46 +++--- tests/options.rs | 46 +++--- tests/positionals.rs | 16 +- 23 files changed, 372 insertions(+), 357 deletions(-) diff --git a/README.md b/README.md index 0cfcbb2..e8e6060 100644 --- a/README.md +++ b/README.md @@ -48,17 +48,17 @@ enum Arg { // The doc strings below will be part of the `--help` text // First we define a simple flag: /// Do not transform input text to uppercase - #[option("-n", "--no-caps")] + #[arg("-n", "--no-caps")] NoCaps, - + // This option takes a value: /// Add exclamation marks to output - #[option("-e N", "--exclaim=N")] + #[arg("-e N", "--exclaim=N")] ExclamationMarks(u8), // This is a positional argument, the range specifies that // at least one positional argument must be passed. - #[positional(1..)] + #[arg("TEXT", 1..)] Text(String), } diff --git a/derive/src/argument.rs b/derive/src/argument.rs index 3854be5..7e3928a 100644 --- a/derive/src/argument.rs +++ b/derive/src/argument.rs @@ -8,18 +8,18 @@ use quote::quote; use syn::{Attribute, Fields, FieldsUnnamed, Ident, Meta, Variant}; use crate::{ - attributes::{parse_argument_attribute, ArgAttr, ArgumentsAttr}, + attributes::{ArgAttr, ArgumentsAttr}, flags::{Flags, Value}, }; -pub(crate) struct Argument { - pub(crate) ident: Ident, - pub(crate) name: String, - pub(crate) arg_type: ArgType, - pub(crate) help: String, +pub struct Argument { + pub ident: Ident, + pub name: String, + pub arg_type: ArgType, + pub help: String, } -pub(crate) enum ArgType { +pub enum ArgType { Option { flags: Flags, hidden: bool, @@ -35,7 +35,7 @@ pub(crate) enum ArgType { }, } -pub(crate) fn parse_arguments_attr(attrs: &[Attribute]) -> ArgumentsAttr { +pub fn parse_arguments_attr(attrs: &[Attribute]) -> ArgumentsAttr { for attr in attrs { if attr.path().is_ident("arguments") { return ArgumentsAttr::parse(attr).unwrap(); @@ -44,7 +44,7 @@ pub(crate) fn parse_arguments_attr(attrs: &[Attribute]) -> ArgumentsAttr { ArgumentsAttr::default() } -pub(crate) fn parse_argument(v: Variant) -> Vec { +pub fn parse_argument(v: Variant) -> Vec { let ident = v.ident; let name = ident.to_string(); let attributes = get_arg_attributes(&v.attrs).unwrap(); @@ -138,16 +138,12 @@ fn collect_help(attrs: &[Attribute]) -> String { fn get_arg_attributes(attrs: &[Attribute]) -> syn::Result> { attrs .iter() - .filter(|a| { - a.path().is_ident("option") - || a.path().is_ident("positional") - || a.path().is_ident("free") - }) - .map(parse_argument_attribute) + .filter(|a| a.path().is_ident("arg")) + .map(ArgAttr::parse) .collect() } -pub(crate) fn short_handling(args: &[Argument]) -> (TokenStream, Vec) { +pub fn short_handling(args: &[Argument]) -> (TokenStream, Vec) { let mut match_arms = Vec::new(); let mut short_flags = Vec::new(); @@ -195,7 +191,7 @@ pub(crate) fn short_handling(args: &[Argument]) -> (TokenStream, Vec) { (token_stream, short_flags) } -pub(crate) fn long_handling(args: &[Argument], help_flags: &Flags) -> TokenStream { +pub fn long_handling(args: &[Argument], help_flags: &Flags) -> TokenStream { let mut match_arms = Vec::new(); let mut options = Vec::new(); @@ -270,7 +266,7 @@ pub(crate) fn long_handling(args: &[Argument], help_flags: &Flags) -> TokenStrea ) } -pub(crate) fn free_handling(args: &[Argument]) -> TokenStream { +pub fn free_handling(args: &[Argument]) -> TokenStream { let mut if_expressions = Vec::new(); // Free arguments @@ -337,7 +333,7 @@ pub(crate) fn free_handling(args: &[Argument]) -> TokenStream { ) } -pub(crate) fn positional_handling(args: &[Argument]) -> (TokenStream, TokenStream) { +pub fn positional_handling(args: &[Argument]) -> (TokenStream, TokenStream) { let mut match_arms = Vec::new(); // The largest index of the previous argument, so the the argument after this should // belong to the next argument. diff --git a/derive/src/attributes.rs b/derive/src/attributes.rs index e8c915c..fc56dc7 100644 --- a/derive/src/attributes.rs +++ b/derive/src/attributes.rs @@ -10,83 +10,12 @@ use syn::{ use crate::flags::Flags; -pub(crate) enum ArgAttr { - Option(OptionAttr), - Positional(PositionalAttr), - Free(FreeAttr), -} - -pub(crate) fn parse_argument_attribute(attr: &Attribute) -> syn::Result { - if attr.path().is_ident("option") { - Ok(ArgAttr::Option(OptionAttr::parse(attr)?)) - } else if attr.path().is_ident("positional") { - Ok(ArgAttr::Positional(PositionalAttr::parse(attr)?)) - } else if attr.path().is_ident("free") { - Ok(ArgAttr::Free(FreeAttr::parse(attr)?)) - } else { - panic!("Internal error: invalid argument attribute"); - } -} - -pub(crate) struct ArgumentsAttr { - pub(crate) help_flags: Flags, - pub(crate) version_flags: Flags, - pub(crate) file: Option, - pub(crate) exit_code: i32, - pub(crate) parse_echo_style: bool, -} - -fn get_ident(meta: &ParseNestedMeta) -> syn::Result { - match meta.path.get_ident() { - Some(ident) => Ok(ident.to_string()), - None => Err(meta.error("expected an identifier")), - } -} - -fn assert_expr_is_array_of_litstr(expr: Expr, flag: &str) -> syn::Result> { - let arr = match expr { - syn::Expr::Array(arr) => arr, - _ => { - return Err(syn::Error::new_spanned( - expr, - format!("Argument to `{flag}` must be an array"), - )) - } - }; - - let mut strings = Vec::new(); - for elem in arr.elems { - let val = match elem { - syn::Expr::Lit(syn::ExprLit { - attrs: _, - lit: syn::Lit::Str(litstr), - }) => litstr.value(), - _ => { - return Err(syn::Error::new_spanned( - elem, - format!("Argument to `{flag}` must be an array of string literals"), - )) - } - }; - strings.push(val); - } - Ok(strings) -} - -fn parse_args( - attr: &Attribute, - mut logic: impl FnMut(ParseStream) -> syn::Result<()>, -) -> syn::Result<()> { - attr.parse_args_with(|s: ParseStream| loop { - logic(s)?; - if s.is_empty() { - return Ok(()); - } - s.parse::()?; - if s.is_empty() { - return Ok(()); - } - }) +pub struct ArgumentsAttr { + pub help_flags: Flags, + pub version_flags: Flags, + pub file: Option, + pub exit_code: i32, + pub parse_echo_style: bool, } impl Default for ArgumentsAttr { @@ -102,7 +31,7 @@ impl Default for ArgumentsAttr { } impl ArgumentsAttr { - pub(crate) fn parse(attr: &Attribute) -> syn::Result { + pub fn parse(attr: &Attribute) -> syn::Result { let mut args = ArgumentsAttr::default(); attr.parse_nested_meta(|meta| { @@ -138,27 +67,57 @@ impl ArgumentsAttr { } } +pub enum ArgAttr { + Option(OptionAttr), + Positional(PositionalAttr), + Free(FreeAttr), +} + +impl ArgAttr { + pub fn parse(attr: &Attribute) -> syn::Result { + assert!(attr.path().is_ident("arg")); + + attr.parse_args_with(|s: ParseStream| { + // Based on the first value, we determine the type of argument. + if let Ok(litstr) = s.parse::() { + let v = litstr.value(); + if v.starts_with('-') || v.contains('=') { + OptionAttr::from_args(v, s).map(Self::Option) + } else { + PositionalAttr::from_args(v, s).map(Self::Positional) + } + } else if let Ok(v) = s.parse::() { + FreeAttr::from_args(v, s).map(Self::Free) + } else { + // TODO: Improve error message + panic!("Could not determine type of argument"); + } + }) + } +} + #[derive(Default)] -pub(crate) struct OptionAttr { - pub(crate) flags: Flags, - pub(crate) parser: Option, - pub(crate) default: Option, - pub(crate) hidden: bool, - pub(crate) help: Option, +pub struct OptionAttr { + pub flags: Flags, + pub parser: Option, + pub default: Option, + pub hidden: bool, + pub help: Option, } impl OptionAttr { - pub(crate) fn parse(attr: &Attribute) -> syn::Result { + fn from_args(first_flag: String, s: ParseStream) -> syn::Result { let mut option_attr = OptionAttr::default(); + option_attr.flags.add(&first_flag); - parse_args(attr, |s: ParseStream| { + parse_args(s, |s: ParseStream| { if let Ok(litstr) = s.parse::() { option_attr.flags.add(&litstr.value()); return Ok(()); } let ident = s.parse::()?; - match ident.to_string().as_str() { + match ident.to_string().as_ref() { "parser" => { s.parse::()?; let p = s.parse::()?; @@ -192,15 +151,16 @@ impl OptionAttr { } #[derive(Default)] -pub(crate) struct FreeAttr { - pub(crate) filters: Vec, +pub struct FreeAttr { + pub filters: Vec, } impl FreeAttr { - pub(crate) fn parse(attr: &Attribute) -> syn::Result { + pub fn from_args(first_value: syn::Ident, s: ParseStream) -> syn::Result { let mut free_attr = FreeAttr::default(); + free_attr.filters.push(first_value); - parse_args(attr, |s: ParseStream| { + parse_args(s, |s: ParseStream| { let ident = s.parse::()?; free_attr.filters.push(ident); Ok(()) @@ -210,46 +170,9 @@ impl FreeAttr { } } -#[derive(Default)] -pub(crate) struct ValueAttr { - pub(crate) keys: Vec, - pub(crate) value: Option, -} - -impl ValueAttr { - pub(crate) fn parse(attr: &Attribute) -> syn::Result { - let mut value_attr = Self::default(); - - // value does not need to take arguments, so short circuit if it does not have one - if let syn::Meta::Path(_) = &attr.meta { - return Ok(value_attr); - } - - parse_args(attr, |s: ParseStream| { - if let Ok(litstr) = s.parse::() { - value_attr.keys.push(litstr.value()); - return Ok(()); - } - - let ident = s.parse::()?; - match ident.to_string().as_str() { - "value" => { - s.parse::()?; - let p = s.parse::()?; - value_attr.value = Some(p); - } - _ => return Err(s.error("unrecognized keyword in value attribute")), - } - Ok(()) - })?; - - Ok(value_attr) - } -} - -pub(crate) struct PositionalAttr { - pub(crate) num_args: RangeInclusive, - pub(crate) last: bool, +pub struct PositionalAttr { + pub num_args: RangeInclusive, + pub last: bool, } impl Default for PositionalAttr { @@ -262,9 +185,9 @@ impl Default for PositionalAttr { } impl PositionalAttr { - pub(crate) fn parse(attr: &Attribute) -> syn::Result { + pub fn from_args(_first_value: String, s: ParseStream) -> syn::Result { let mut positional_attr = Self::default(); - parse_args(attr, |s| { + parse_args(s, |s| { if (s.peek(LitInt) && s.peek2(Token![..])) || s.peek(Token![..]) { let range = s.parse::()?; // We're dealing with a range @@ -322,3 +245,99 @@ impl PositionalAttr { Ok(positional_attr) } } + +#[derive(Default)] +pub struct ValueAttr { + pub keys: Vec, + pub value: Option, +} + +impl ValueAttr { + pub fn parse(attr: &Attribute) -> syn::Result { + let mut value_attr = Self::default(); + + // value does not need to take arguments, so short circuit if it does not have one + if let syn::Meta::Path(_) = &attr.meta { + return Ok(value_attr); + } + + attr.parse_args_with(|s: ParseStream| loop { + if let Ok(litstr) = s.parse::() { + value_attr.keys.push(litstr.value()); + } else { + let ident = s.parse::()?; + match ident.to_string().as_str() { + "value" => { + s.parse::()?; + let p = s.parse::()?; + value_attr.value = Some(p); + } + _ => return Err(s.error("unrecognized keyword in value attribute")), + } + } + + if s.is_empty() { + return Ok(()); + } + s.parse::()?; + if s.is_empty() { + return Ok(()); + } + })?; + + Ok(value_attr) + } +} + +fn parse_args( + s: ParseStream, + mut logic: impl FnMut(ParseStream) -> syn::Result<()>, +) -> syn::Result<()> { + loop { + if s.is_empty() { + return Ok(()); + } + s.parse::()?; + if s.is_empty() { + return Ok(()); + } + logic(s)?; + } +} + +fn get_ident(meta: &ParseNestedMeta) -> syn::Result { + match meta.path.get_ident() { + Some(ident) => Ok(ident.to_string()), + None => Err(meta.error("expected an identifier")), + } +} + +fn assert_expr_is_array_of_litstr(expr: Expr, flag: &str) -> syn::Result> { + let arr = match expr { + syn::Expr::Array(arr) => arr, + _ => { + return Err(syn::Error::new_spanned( + expr, + format!("Argument to `{flag}` must be an array"), + )) + } + }; + + let mut strings = Vec::new(); + for elem in arr.elems { + let val = match elem { + syn::Expr::Lit(syn::ExprLit { + attrs: _, + lit: syn::Lit::Str(litstr), + }) => litstr.value(), + _ => { + return Err(syn::Error::new_spanned( + elem, + format!("Argument to `{flag}` must be an array of string literals"), + )) + } + }; + strings.push(val); + } + Ok(strings) +} diff --git a/derive/src/flags.rs b/derive/src/flags.rs index 7cab9e5..82cf489 100644 --- a/derive/src/flags.rs +++ b/derive/src/flags.rs @@ -5,27 +5,27 @@ use proc_macro2::TokenStream; use quote::quote; #[derive(Default)] -pub(crate) struct Flags { +pub struct Flags { pub short: Vec>, pub long: Vec>, pub dd_style: Vec<(String, String)>, } #[derive(Clone)] -pub(crate) enum Value { +pub enum Value { No, Optional(String), Required(String), } #[derive(Clone)] -pub(crate) struct Flag { - pub(crate) flag: T, - pub(crate) value: Value, +pub struct Flag { + pub flag: T, + pub value: Value, } impl Flags { - pub(crate) fn new>(flags: impl IntoIterator) -> Self { + pub fn new>(flags: impl IntoIterator) -> Self { let mut self_ = Self::default(); for flag in flags { self_.add(flag.as_ref()); @@ -33,7 +33,7 @@ impl Flags { self_ } - pub(crate) fn add(&mut self, flag: &str) { + pub fn add(&mut self, flag: &str) { if let Some(s) = flag.strip_prefix("--") { // There are three possible patterns: // --flag @@ -110,11 +110,11 @@ impl Flags { } } - pub(crate) fn is_empty(&self) -> bool { + pub fn is_empty(&self) -> bool { self.short.is_empty() && self.long.is_empty() && self.dd_style.is_empty() } - pub(crate) fn pat(&self) -> TokenStream { + pub fn pat(&self) -> TokenStream { let short: Vec<_> = self.short.iter().map(|f| f.flag).collect(); let long: Vec<_> = self.long.iter().map(|f| &f.flag).collect(); match (&short[..], &long[..]) { @@ -127,7 +127,7 @@ impl Flags { } } - pub(crate) fn format(&self) -> String { + pub fn format(&self) -> String { let short = self .short .iter() diff --git a/derive/src/help.rs b/derive/src/help.rs index 8348ad0..4ab16ba 100644 --- a/derive/src/help.rs +++ b/derive/src/help.rs @@ -14,7 +14,7 @@ use crate::{ use proc_macro2::TokenStream; use quote::quote; -pub(crate) fn help_handling(help_flags: &Flags) -> TokenStream { +pub fn help_handling(help_flags: &Flags) -> TokenStream { if help_flags.is_empty() { return quote!(); } @@ -28,7 +28,7 @@ pub(crate) fn help_handling(help_flags: &Flags) -> TokenStream { ) } -pub(crate) fn help_string( +pub fn help_string( args: &[Argument], help_flags: &Flags, version_flags: &Flags, @@ -144,7 +144,7 @@ fn read_help_file(file: &str) -> (String, String, String) { ) } -pub(crate) fn version_handling(version_flags: &Flags) -> TokenStream { +pub fn version_handling(version_flags: &Flags) -> TokenStream { if version_flags.is_empty() { return quote!(); } diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 8c3d4cc..7383602 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -24,7 +24,7 @@ pub fn initial(input: TokenStream) -> TokenStream { initial::initial(input) } -#[proc_macro_derive(Arguments, attributes(flag, option, positional, free, arguments))] +#[proc_macro_derive(Arguments, attributes(arg, arguments))] pub fn arguments(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); diff --git a/examples/deprecated.rs b/examples/deprecated.rs index 0e5cdde..ad92a42 100644 --- a/examples/deprecated.rs +++ b/examples/deprecated.rs @@ -20,10 +20,10 @@ fn parse_plus(s: &str) -> Option<&str> { #[derive(Arguments)] enum Arg { - #[free(parse_minus)] + #[arg(parse_minus)] Min(usize), - #[free(parse_plus)] + #[arg(parse_plus)] Plus(isize), } diff --git a/examples/hello_world.rs b/examples/hello_world.rs index d37425c..5d95993 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -8,15 +8,15 @@ enum Arg { /// Just to show off, I can do multiple paragraphs and wrap text! /// /// # Also headings! - #[option("-n NAME", "--name=NAME", "name=NAME")] + #[arg("-n NAME", "--name=NAME", "name=NAME")] Name(String), /// The **number of times** to `greet` - #[option("-c N", "--count=N")] + #[arg("-c N", "--count=N")] Count(u8), /// This argument is hidden - #[option("--hidden", hidden)] + #[arg("--hidden", hidden)] Hidden, } diff --git a/tests/coreutils/b2sum.rs b/tests/coreutils/b2sum.rs index 52c8036..d32a183 100644 --- a/tests/coreutils/b2sum.rs +++ b/tests/coreutils/b2sum.rs @@ -3,31 +3,31 @@ use uutils_args::{Arguments, Initial, Options}; #[derive(Clone, Arguments)] enum Arg { - #[option("-b", "--binary")] + #[arg("-b", "--binary")] Binary, - #[option("-c", "--check")] + #[arg("-c", "--check")] Check, - #[option("--tag")] + #[arg("--tag")] Tag, - #[option("-t", "--text")] + #[arg("-t", "--text")] Text, - #[option("-q", "--quiet")] + #[arg("-q", "--quiet")] Quiet, - #[option("-s", "--status")] + #[arg("-s", "--status")] Status, - #[option("--strict")] + #[arg("--strict")] Strict, - #[option("-w", "--warn")] + #[arg("-w", "--warn")] Warn, - #[positional(..)] + #[arg("FILE", ..)] File(PathBuf), } diff --git a/tests/coreutils/base32.rs b/tests/coreutils/base32.rs index 240d9d0..6681daf 100644 --- a/tests/coreutils/base32.rs +++ b/tests/coreutils/base32.rs @@ -4,16 +4,16 @@ use uutils_args::{Arguments, Initial, Options}; #[derive(Clone, Arguments)] enum Arg { - #[option("-d", "--decode")] + #[arg("-d", "--decode")] Decode, - #[option("-i", "--ignore-garbage")] + #[arg("-i", "--ignore-garbage")] IgnoreGarbage, - #[option("-w COLS", "--wrap=COLS")] + #[arg("-w COLS", "--wrap=COLS")] Wrap(usize), - #[positional(..=1)] + #[arg("FILE", ..=1)] File(PathBuf), } diff --git a/tests/coreutils/basename.rs b/tests/coreutils/basename.rs index 231a9fc..404ad60 100644 --- a/tests/coreutils/basename.rs +++ b/tests/coreutils/basename.rs @@ -2,16 +2,16 @@ use uutils_args::{Arguments, Initial, Options}; #[derive(Clone, Arguments)] enum Arg { - #[option("-a", "--multiple")] + #[arg("-a", "--multiple")] Multiple, - #[option("-s SUFFIX", "--suffix=SUFFIX")] + #[arg("-s SUFFIX", "--suffix=SUFFIX")] Suffix(String), - #[option("-z", "--zero")] + #[arg("-z", "--zero")] Zero, - #[positional(last, ..)] + #[arg("NAMES", last, ..)] Names(Vec), } diff --git a/tests/coreutils/cat.rs b/tests/coreutils/cat.rs index d4c9928..5932f6c 100644 --- a/tests/coreutils/cat.rs +++ b/tests/coreutils/cat.rs @@ -12,34 +12,34 @@ enum NumberingMode { #[derive(Clone, Arguments)] enum Arg { - #[option("-A", "--show-all")] + #[arg("-A", "--show-all")] ShowAll, - #[option("-b", "--number-nonblank")] + #[arg("-b", "--number-nonblank")] NumberNonblank, - #[option("-e")] + #[arg("-e")] ShowNonPrintingEnds, - #[option("-E")] + #[arg("-E")] ShowEnds, - #[option("-n", "--number")] + #[arg("-n", "--number")] Number, - #[option("-s", "--squeeze-blank")] + #[arg("-s", "--squeeze-blank")] SqueezeBlank, - #[option("-t")] + #[arg("-t")] ShowNonPrintingTabs, - #[option("-T", "--show-tabs")] + #[arg("-T", "--show-tabs")] ShowTabs, - #[option("-v", "--show-nonprinting")] + #[arg("-v", "--show-nonprinting")] ShowNonPrinting, - #[positional(..)] + #[arg("FILES", ..)] File(PathBuf), } diff --git a/tests/coreutils/dd.rs b/tests/coreutils/dd.rs index 723af69..ebac8f0 100644 --- a/tests/coreutils/dd.rs +++ b/tests/coreutils/dd.rs @@ -16,43 +16,43 @@ enum StatusLevel { // TODO: The bytes arguments should parse sizes #[derive(Arguments)] enum Arg { - #[option("if=FILE")] + #[arg("if=FILE")] Infile(PathBuf), - #[option("of=FILE")] + #[arg("of=FILE")] Outfile(PathBuf), - #[option("ibs=BYTES")] + #[arg("ibs=BYTES")] Ibs(usize), - #[option("obs=BYTES")] + #[arg("obs=BYTES")] Obs(usize), - #[option("bs=BYTES")] + #[arg("bs=BYTES")] Bs(usize), - #[option("cbs=BYTES")] + #[arg("cbs=BYTES")] Cbs(usize), - #[option("skip=BYTES", "iseek=BYTES")] + #[arg("skip=BYTES", "iseek=BYTES")] Skip(u64), - #[option("seek=BYTES", "oseek=BYTES")] + #[arg("seek=BYTES", "oseek=BYTES")] Seek(u64), - #[option("count=N")] + #[arg("count=N")] Count(usize), - #[option("status=LEVEL")] + #[arg("status=LEVEL")] Status(StatusLevel), - #[option("conv=CONVERSIONS")] + #[arg("conv=CONVERSIONS")] Conv(String), - #[option("iflag=FLAGS")] + #[arg("iflag=FLAGS")] Iflag(String), - #[option("oflag=FLAGS")] + #[arg("oflag=FLAGS")] Oflag(String), } diff --git a/tests/coreutils/echo.rs b/tests/coreutils/echo.rs index d42fe69..2cc1753 100644 --- a/tests/coreutils/echo.rs +++ b/tests/coreutils/echo.rs @@ -5,18 +5,18 @@ use uutils_args::{Arguments, Initial, Options}; #[arguments(parse_echo_style)] enum Arg { /// Do not output trailing newline - #[option("-n")] + #[arg("-n")] NoNewline, /// Enable interpretation of backslash escapes - #[option("-e")] + #[arg("-e")] EnableEscape, /// Disable interpretation of backslash escapes - #[option("-E")] + #[arg("-E")] DisableEscape, - #[positional(last)] + #[arg("STRING", last)] String(Vec), } diff --git a/tests/coreutils/head.rs b/tests/coreutils/head.rs index 07e35df..abe8ad4 100644 --- a/tests/coreutils/head.rs +++ b/tests/coreutils/head.rs @@ -84,22 +84,22 @@ where #[derive(Arguments)] enum Arg { - #[option("-c NUM", "--bytes=NUM")] + #[arg("-c NUM", "--bytes=NUM")] Bytes(SigNum), - #[option("-n NUM", "--lines=NUM")] + #[arg("-n NUM", "--lines=NUM")] Lines(SigNum), - #[option("-q", "--quiet", "--silent")] + #[arg("-q", "--quiet", "--silent")] Quiet, - #[option("-v", "--verbose")] + #[arg("-v", "--verbose")] Verbose, - #[option("-z", "--zero-terminated")] + #[arg("-z", "--zero-terminated")] Zero, - #[positional(..)] + #[arg("FILES", ..)] File(PathBuf), } diff --git a/tests/coreutils/ls.rs b/tests/coreutils/ls.rs index 4a71000..75f3a9f 100644 --- a/tests/coreutils/ls.rs +++ b/tests/coreutils/ls.rs @@ -134,150 +134,150 @@ enum IndicatorStyle { enum Arg { // === Files === /// Do not ignore entries starting with . - #[option("-a")] + #[arg("-a")] All, /// Do not list implied . and .. - #[option("-A")] + #[arg("-A")] AlmostAll, /// Show file author (ignored) - #[option("--author")] + #[arg("--author")] Author, - #[option("--time=WORD")] - #[option("-c", default = Time::Change)] - #[option("-u", default = Time::Access)] + #[arg("--time=WORD")] + #[arg("-c", default = Time::Change)] + #[arg("-u", default = Time::Access)] Time(Time), // === Sorting == /// Sort by WORD - #[option("--sort=WORD")] - #[option("-t", default = Sort::Time, help = "Sort by time")] - #[option("-U", default = Sort::None, help = "Do not sort")] - #[option("-v", default = Sort::Version, help = "Sort by version")] - #[option("-X", default = Sort::Extension, help = "Sort by extension")] + #[arg("--sort=WORD")] + #[arg("-t", default = Sort::Time, help = "Sort by time")] + #[arg("-U", default = Sort::None, help = "Do not sort")] + #[arg("-v", default = Sort::Version, help = "Sort by version")] + #[arg("-X", default = Sort::Extension, help = "Sort by extension")] Sort(Sort), // === Miscellaneous === - #[option("-Z", "--context")] + #[arg("-Z", "--context")] SecurityContext, /// Do not list files starting with ~ - #[option("-B", "--ignore-backups")] + #[arg("-B", "--ignore-backups")] IgnoreBackups, - #[option("-d", "--directory")] + #[arg("-d", "--directory")] Directory, - #[option("-D", "--dired")] + #[arg("-D", "--dired")] Dired, - #[option("--hyperlink")] + #[arg("--hyperlink")] Hyperlink(When), - #[option("-i", "--inode")] + #[arg("-i", "--inode")] Inode, - #[option("-I PATTERN", "--ignore=PATTERN")] + #[arg("-I PATTERN", "--ignore=PATTERN")] Ignore(String), - #[option("-r", "--reverse")] + #[arg("-r", "--reverse")] Reverse, - #[option("-R", "--recursive")] + #[arg("-R", "--recursive")] Recursive, - #[option("-w COLS", "--width=COLS")] + #[arg("-w COLS", "--width=COLS")] Width(u16), - #[option("-s", "--size")] + #[arg("-s", "--size")] AllocationSize, - #[option("-G", "--no-group")] + #[arg("-G", "--no-group")] NoGroup, // === Format === /// Set format - #[option("--format=FORMAT")] - #[option("-l", "--long", default = Format::Long, help = "Use long format")] - #[option("-C", default = Format::Columns, help = "Use columns format")] - #[option("-x", default = Format::Across, help = "Use across format")] - #[option("-m", default = Format::Commas, help = "Use comma format")] + #[arg("--format=FORMAT")] + #[arg("-l", "--long", default = Format::Long, help = "Use long format")] + #[arg("-C", default = Format::Columns, help = "Use columns format")] + #[arg("-x", default = Format::Across, help = "Use across format")] + #[arg("-m", default = Format::Commas, help = "Use comma format")] Format(Format), /// Show single column - #[option("-1")] + #[arg("-1")] SingleColumn, - #[option("-o")] + #[arg("-o")] LongNoGroup, - #[option("-g")] + #[arg("-g")] LongNoOwner, - #[option("-n", "--numeric-uid-gid")] + #[arg("-n", "--numeric-uid-gid")] LongNumericUidGid, // === Indicator style === - #[option("--indicator-style=STYLE")] - #[option("-p", default = IndicatorStyle::Slash, help = "Append slash to directories")] - #[option("--file-type", default = IndicatorStyle::FileType, help = "Add indicators for file types")] + #[arg("--indicator-style=STYLE")] + #[arg("-p", default = IndicatorStyle::Slash, help = "Append slash to directories")] + #[arg("--file-type", default = IndicatorStyle::FileType, help = "Add indicators for file types")] IndicatorStyle(IndicatorStyle), /// Classify items - #[option("-F", "--classify[=WHEN]", default = When::Always)] + #[arg("-F", "--classify[=WHEN]", default = When::Always)] IndicatorStyleClassify(When), // === Dereference === - #[option("-L", "--dereference")] + #[arg("-L", "--dereference")] DerefAll, - #[option("--dereference-command-line-symlink-to-dir")] + #[arg("--dereference-command-line-symlink-to-dir")] DerefDirArgs, - #[option("--dereference-command-line")] + #[arg("--dereference-command-line")] DerefArgs, // === Size === - #[option("-h", "--human-readable")] + #[arg("-h", "--human-readable")] HumanReadable, - #[option("-k", "--kibibytes")] + #[arg("-k", "--kibibytes")] Kibibytes, - #[option("--si")] + #[arg("--si")] Si, - // #[option("--block-size=BLOCKSIZE")] + // #[arg("--block-size=BLOCKSIZE")] // BlockSize(Size), // === Quoting style === - #[option("--quoting-style=STYLE")] - #[option("-N", "--literal", default = QuotingStyle::Literal)] - #[option("-h", "--escape", default = QuotingStyle::Escape)] - #[option("-Q", "--quote-name", default = todo!())] + #[arg("--quoting-style=STYLE")] + #[arg("-N", "--literal", default = QuotingStyle::Literal)] + #[arg("-h", "--escape", default = QuotingStyle::Escape)] + #[arg("-Q", "--quote-name", default = todo!())] QuotingStyle(QuotingStyle), /// Set the color - #[option("--color[=WHEN]", default = When::Always)] + #[arg("--color[=WHEN]", default = When::Always)] Color(When), /// Print control characters as ? - #[option("-q", "--hide-control-chars")] + #[arg("-q", "--hide-control-chars")] HideControlChars, /// Show control characters as is - #[option("--show-control-chars")] + #[arg("--show-control-chars")] ShowControlChars, - #[option("--zero")] + #[arg("--zero")] Zero, - #[option("--group-directories-first")] + #[arg("--group-directories-first")] GroupDirectoriesFirst, - #[positional(..)] + #[arg("FILES", ..)] File(PathBuf), } diff --git a/tests/coreutils/mktemp.rs b/tests/coreutils/mktemp.rs index b80b352..ad18b74 100644 --- a/tests/coreutils/mktemp.rs +++ b/tests/coreutils/mktemp.rs @@ -4,25 +4,25 @@ use uutils_args::{Arguments, Initial, Options}; #[derive(Clone, Arguments)] enum Arg { - #[option("-d", "--directory")] + #[arg("-d", "--directory")] Directory, - #[option("-u", "--dry-run")] + #[arg("-u", "--dry-run")] DryRun, - #[option("-q", "--quiet")] + #[arg("-q", "--quiet")] Quiet, - #[option("--suffix=SUFFIX")] + #[arg("--suffix=SUFFIX")] Suffix(String), - #[option("-t")] + #[arg("-t")] TreatAsTemplate, - #[option("-p DIR", "--tmpdir[=DIR]", default = ".".into())] + #[arg("-p DIR", "--tmpdir[=DIR]", default = ".".into())] TmpDir(PathBuf), - #[positional(0..=1)] + #[arg("TEMPLATE", 0..=1)] Template(String), } diff --git a/tests/coreutils/tail.rs b/tests/coreutils/tail.rs index 244dfcb..c23ba7b 100644 --- a/tests/coreutils/tail.rs +++ b/tests/coreutils/tail.rs @@ -106,44 +106,44 @@ where #[derive(Arguments)] enum Arg { - #[option("-c NUM", "--bytes=NUM")] + #[arg("-c NUM", "--bytes=NUM")] Bytes(SigNum), - #[option("-f", "--follow[=HOW]", default=FollowMode::Descriptor)] + #[arg("-f", "--follow[=HOW]", default=FollowMode::Descriptor)] Follow(FollowMode), - #[option("-F")] + #[arg("-F")] FollowRetry, - #[option("--max-unchanged-stats=N")] + #[arg("--max-unchanged-stats=N")] MaxUnchangedStats(u32), - #[option("-n NUM", "--lines=NUM")] + #[arg("-n NUM", "--lines=NUM")] Lines(SigNum), - #[option("--pid=PID")] + #[arg("--pid=PID")] Pid(u64), - #[option("-q", "--quiet", "--silent")] + #[arg("-q", "--quiet", "--silent")] Quiet, - #[option("--retry")] + #[arg("--retry")] Retry, - #[option("-s NUMBER", "--sleep-interval=NUMBER")] + #[arg("-s NUMBER", "--sleep-interval=NUMBER")] SleepInterval(u64), - #[option("-v", "--verbose")] + #[arg("-v", "--verbose")] Verbose, - #[option("-z", "--zero-terminated")] + #[arg("-z", "--zero-terminated")] Zero, - #[positional(..)] - File(PathBuf), - - #[option("---presume-input-pipe", hidden)] + #[arg("---presume-input-pipe", hidden)] PresumeInputPipe, + + #[arg("FILES", ..)] + File(PathBuf), } // We need both negative and positive 0 diff --git a/tests/defaults.rs b/tests/defaults.rs index 21ddd05..76a6511 100644 --- a/tests/defaults.rs +++ b/tests/defaults.rs @@ -4,7 +4,7 @@ use uutils_args::{Arguments, Initial, Options}; fn true_default() { #[derive(Arguments)] enum Arg { - #[option("--foo")] + #[arg("--foo")] Foo, } @@ -28,7 +28,7 @@ fn true_default() { fn env_var_string() { #[derive(Arguments)] enum Arg { - #[option("--foo=MSG")] + #[arg("--foo=MSG")] Foo(String), } diff --git a/tests/exit_code.rs b/tests/exit_code.rs index 76e2a66..ace64c6 100644 --- a/tests/exit_code.rs +++ b/tests/exit_code.rs @@ -5,7 +5,7 @@ fn one_flag() { #[derive(Arguments, Clone, Debug, PartialEq, Eq)] #[arguments(exit_code = 4)] enum Arg { - #[option("-f", "--foo")] + #[arg("-f", "--foo")] Foo, } diff --git a/tests/flags.rs b/tests/flags.rs index 0d4b1fe..2a6b808 100644 --- a/tests/flags.rs +++ b/tests/flags.rs @@ -4,7 +4,7 @@ use uutils_args::{Arguments, Initial, Options}; fn one_flag() { #[derive(Arguments)] enum Arg { - #[option("-f", "--foo")] + #[arg("-f", "--foo")] Foo, } @@ -29,9 +29,9 @@ fn one_flag() { fn two_flags() { #[derive(Arguments, Clone)] enum Arg { - #[option("-a")] + #[arg("-a")] A, - #[option("-b")] + #[arg("-b")] B, } @@ -69,7 +69,7 @@ fn two_flags() { fn long_and_short_flag() { #[derive(Arguments)] enum Arg { - #[option("-f", "--foo")] + #[arg("-f", "--foo")] Foo, } @@ -93,7 +93,7 @@ fn long_and_short_flag() { fn short_alias() { #[derive(Arguments)] enum Arg { - #[option("-b")] + #[arg("-b")] Foo, } @@ -115,7 +115,7 @@ fn short_alias() { fn long_alias() { #[derive(Arguments)] enum Arg { - #[option("--bar")] + #[arg("--bar")] Foo, } @@ -137,9 +137,9 @@ fn long_alias() { fn short_and_long_alias() { #[derive(Arguments)] enum Arg { - #[option("-b", "--bar")] + #[arg("-b", "--bar")] Foo, - #[option("-f", "--foo")] + #[arg("-f", "--foo")] Bar, } @@ -178,11 +178,11 @@ fn short_and_long_alias() { fn xyz_map_to_abc() { #[derive(Arguments)] enum Arg { - #[option("-x")] + #[arg("-x")] X, - #[option("-y")] + #[arg("-y")] Y, - #[option("-z")] + #[arg("-z")] Z, } @@ -254,9 +254,9 @@ fn xyz_map_to_abc() { fn non_rust_ident() { #[derive(Arguments)] enum Arg { - #[option("--foo-bar")] + #[arg("--foo-bar")] FooBar, - #[option("--super")] + #[arg("--super")] Super, } @@ -285,7 +285,7 @@ fn non_rust_ident() { fn number_flag() { #[derive(Arguments, Clone)] enum Arg { - #[option("-1")] + #[arg("-1")] One, } #[derive(Initial, PartialEq, Eq, Debug)] @@ -306,9 +306,9 @@ fn number_flag() { fn false_bool() { #[derive(Arguments)] enum Arg { - #[option("-a")] + #[arg("-a")] A, - #[option("-b")] + #[arg("-b")] B, } @@ -338,7 +338,7 @@ fn false_bool() { fn verbosity() { #[derive(Arguments)] enum Arg { - #[option("-v")] + #[arg("-v")] Verbosity, } @@ -362,11 +362,11 @@ fn verbosity() { fn infer_long_args() { #[derive(Arguments)] enum Arg { - #[option("--all")] + #[arg("--all")] All, - #[option("--almost-all")] + #[arg("--almost-all")] AlmostAll, - #[option("--author")] + #[arg("--author")] Author, } @@ -405,11 +405,11 @@ fn enum_flag() { #[derive(Arguments)] enum Arg { - #[option("--foo")] + #[arg("--foo")] Foo, - #[option("--bar")] + #[arg("--bar")] Bar, - #[option("--baz")] + #[arg("--baz")] Baz, } diff --git a/tests/options.rs b/tests/options.rs index 451c43c..ba02512 100644 --- a/tests/options.rs +++ b/tests/options.rs @@ -6,7 +6,7 @@ use uutils_args::{Arguments, Initial, Options, Value, ValueResult}; fn string_option() { #[derive(Arguments)] enum Arg { - #[option("--message=MSG")] + #[arg("--message=MSG")] Message(String), } @@ -42,7 +42,7 @@ fn enum_option() { #[derive(Arguments)] enum Arg { - #[option("--format=FORMAT")] + #[arg("--format=FORMAT")] Format(Format), } @@ -81,7 +81,7 @@ fn enum_option_with_fields() { #[derive(Arguments)] enum Arg { - #[option("-i INDENT")] + #[arg("-i INDENT")] Indent(Indent), } @@ -130,7 +130,7 @@ fn enum_with_complex_from_value() { #[derive(Arguments)] enum Arg { - #[option("-i INDENT")] + #[arg("-i INDENT")] Indent(Indent), } @@ -164,7 +164,7 @@ fn color() { #[derive(Arguments)] enum Arg { - #[option("--color[=WHEN]")] + #[arg("--color[=WHEN]")] Color(Option), } @@ -201,11 +201,11 @@ fn color() { fn actions() { #[derive(Arguments)] enum Arg { - #[option("-m MESSAGE")] + #[arg("-m MESSAGE")] Message(String), - #[option("--send")] + #[arg("--send")] Send, - #[option("--receive")] + #[arg("--receive")] Receive, } @@ -239,7 +239,7 @@ fn actions() { fn width() { #[derive(Arguments)] enum Arg { - #[option("-w WIDTH")] + #[arg("-w WIDTH")] Width(u64), } @@ -265,25 +265,25 @@ fn width() { fn integers() { #[derive(Arguments)] enum Arg { - #[option("--u8=N")] + #[arg("--u8=N")] U8(u8), - #[option("--u16=N")] + #[arg("--u16=N")] U16(u16), - #[option("--u32=N")] + #[arg("--u32=N")] U32(u32), - #[option("--u64=N")] + #[arg("--u64=N")] U64(u64), - #[option("--u128=N")] + #[arg("--u128=N")] U128(u128), - #[option("--i8=N")] + #[arg("--i8=N")] I8(i8), - #[option("--i16=N")] + #[arg("--i16=N")] I16(i16), - #[option("--i32=N")] + #[arg("--i32=N")] I32(i32), - #[option("--i64=N")] + #[arg("--i64=N")] I64(i64), - #[option("--i128=N")] + #[arg("--i128=N")] I128(i128), } @@ -337,7 +337,7 @@ fn ls_classify() { #[derive(Arguments)] enum Arg { - #[option( + #[arg( "-F", "--classify[=WHEN]", default = When::Always, )] @@ -372,7 +372,7 @@ fn ls_classify() { fn mktemp_tmpdir() { #[derive(Clone, Arguments)] enum Arg { - #[option( + #[arg( "-p DIR", "--tmpdir[=DIR]", default = String::from("/tmp"), )] @@ -445,10 +445,10 @@ fn deprecated() { #[derive(Arguments)] enum Arg { - #[free(parse_minus)] + #[arg(parse_minus)] Min(usize), - #[free(parse_plus)] + #[arg(parse_plus)] Plus(isize), } diff --git a/tests/positionals.rs b/tests/positionals.rs index 5e0bd3b..04f187c 100644 --- a/tests/positionals.rs +++ b/tests/positionals.rs @@ -4,7 +4,7 @@ use uutils_args::{Arguments, Initial, Options}; fn one_positional() { #[derive(Arguments, Clone)] enum Arg { - #[positional(1)] + #[arg("FILE", 1)] File1(String), } @@ -29,9 +29,9 @@ fn one_positional() { fn two_positionals() { #[derive(Arguments)] enum Arg { - #[positional(1)] + #[arg("FOO", 1)] Foo(String), - #[positional(1)] + #[arg("BAR", 1)] Bar(String), } @@ -61,7 +61,7 @@ fn two_positionals() { fn optional_positional() { #[derive(Arguments)] enum Arg { - #[positional(0..=1)] + #[arg("FOO", 0..=1)] Foo(String), } @@ -86,7 +86,7 @@ fn optional_positional() { fn collect_positional() { #[derive(Arguments, Clone)] enum Arg { - #[positional(..)] + #[arg("FOO", ..)] Foo(String), } @@ -111,7 +111,7 @@ fn collect_positional() { fn last1() { #[derive(Arguments)] enum Arg { - #[positional(last, ..)] + #[arg("FOO", last, ..)] Foo(Vec), } @@ -134,10 +134,10 @@ fn last1() { fn last2() { #[derive(Arguments, Clone)] enum Arg { - #[option("-a")] + #[arg("-a")] A, - #[positional(last, ..)] + #[arg("FOO", last, ..)] Foo(Vec), } From 21d2e92d017bbc2bf18329a05d534ebdd6fd2192 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 09:16:04 +0000 Subject: [PATCH 013/116] fix(deps): update rust crate quote to 1.0.33 --- derive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index f55e40f..0901f50 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -10,5 +10,5 @@ proc_macro = true [dependencies] proc-macro2 = "1.0.70" -quote = "1.0.21" +quote = "1.0.33" syn = { version = "2.0.39", features = ["full"] } From 8cf652dfd2404f71caa5e16fb321f7ff827e9297 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Tue, 5 Dec 2023 09:28:04 +0100 Subject: [PATCH 014/116] reference clap (#52) * reference clap * improve wording Co-authored-by: Daniel Hofstetter --------- Co-authored-by: Daniel Hofstetter --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e8e6060..5025c22 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ mimic the behaviour of the GNU coreutils. There are other applications that have similar behaviour, which are C application that use `getopt` and `getopt_long`. If you want to mimic that behaviour exactly, this is the library for you. If you want to write basically anything else, -you should probably pick another argument parser. +you should probably pick another argument parser (for example: [clap](https://github.com/clap-rs/clap)). ## Getting Started From 2ba46a2ae9cd244c04d0af61ee6905457cc46e89 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Sat, 2 Dec 2023 17:40:52 +0100 Subject: [PATCH 015/116] remove the Initial trait --- README.md | 44 +++++------- derive/src/initial.rs | 136 ------------------------------------ derive/src/lib.rs | 6 -- examples/deprecated.rs | 12 ++-- examples/hello_world.rs | 11 +-- src/lib.rs | 36 +++------- tests/coreutils/b2sum.rs | 58 +++++++++------ tests/coreutils/base32.rs | 29 ++++++-- tests/coreutils/basename.rs | 6 +- tests/coreutils/cat.rs | 20 +++--- tests/coreutils/dd.rs | 58 ++++++++++----- tests/coreutils/echo.rs | 10 +-- tests/coreutils/head.rs | 6 +- tests/coreutils/ls.rs | 83 +++++++++++++++------- tests/coreutils/mktemp.rs | 24 +++---- tests/coreutils/tail.rs | 8 +-- tests/coreutils/uniq.rs | 2 +- tests/defaults.rs | 55 --------------- tests/flags.rs | 109 ++++++++++++++++------------- tests/options.rs | 127 +++++++++++++++++++-------------- tests/positionals.rs | 36 +++++----- 21 files changed, 389 insertions(+), 487 deletions(-) delete mode 100644 derive/src/initial.rs delete mode 100644 tests/defaults.rs diff --git a/README.md b/README.md index 5025c22..7119bb7 100644 --- a/README.md +++ b/README.md @@ -41,16 +41,16 @@ For more information on these traits, see their respective documentation: Below is a minimal example of a full CLI application using this library. ```rust -use uutils_args::{Arguments, Initial, Options}; +use uutils_args::{Arguments, Options}; #[derive(Arguments)] enum Arg { // The doc strings below will be part of the `--help` text // First we define a simple flag: - /// Do not transform input text to uppercase - #[arg("-n", "--no-caps")] - NoCaps, - + /// Transform input text to uppercase + #[arg("-c", "--caps")] + Caps, + // This option takes a value: /// Add exclamation marks to output #[arg("-e N", "--exclaim=N")] @@ -62,10 +62,8 @@ enum Arg { Text(String), } -#[derive(Initial)] +#[derive(Default)] struct Settings { - // We can change the default value with the field attribute. - #[initial(true)] caps: bool, exclamation_marks: u8, text: String, @@ -76,7 +74,7 @@ struct Settings { impl Options for Settings { fn apply(&mut self, arg: Arg) { match arg { - Arg::NoCaps => self.caps = false, + Arg::Caps => self.caps = true, Arg::ExclamationMarks(n) => self.exclamation_marks += n, Arg::Text(s) => { if self.text.is_empty() { @@ -91,7 +89,7 @@ impl Options for Settings { } fn run(args: &'static [&'static str]) -> String { - let s = Settings::parse(args); + let s = Settings::default().parse(args); let mut output = if s.caps { s.text.to_uppercase() } else { @@ -104,24 +102,20 @@ fn run(args: &'static [&'static str]) -> String { } // The first argument is the binary name. In this example it's ignored. -assert_eq!(run(&["shout", "hello"]), "HELLO"); -assert_eq!(run(&["shout", "-e3", "hello"]), "HELLO!!!"); -assert_eq!(run(&["shout", "-e", "3", "hello"]), "HELLO!!!"); -assert_eq!(run(&["shout", "--no-caps", "hello"]), "hello"); -assert_eq!(run(&["shout", "-e3", "-n", "hello"]), "hello!!!"); -assert_eq!(run(&["shout", "-e3", "hello", "world"]), "HELLO WORLD!!!"); +assert_eq!(run(&["shout", "hello"]), "hello"); +assert_eq!(run(&["shout", "-e3", "hello"]), "hello!!!"); +assert_eq!(run(&["shout", "-e", "3", "hello"]), "hello!!!"); +assert_eq!(run(&["shout", "--caps", "hello"]), "HELLO"); +assert_eq!(run(&["shout", "-e3", "-c", "hello"]), "HELLO!!!"); +assert_eq!(run(&["shout", "-e3", "-c", "hello", "world"]), "HELLO WORLD!!!"); ``` -## Additional functionality +## Value parsing -To make it easier to implement [`Arguments`] and [`Options`], there are -two additional traits: - -- [`Initial`] is an alternative to the [`Default`] trait from the standard - library, with a richer derive macro. -- [`Value`] allows for easy parsing from `OsStr` to any type - implementing [`Value`]. This crate also provides a derive macro for - this trait. +To make it easier to implement [`Arguments`] and [`Options`], there is the +[`Value`] trait, which allows for easy parsing from `OsStr` to any type +implementing [`Value`]. This crate also provides a derive macro for +this trait. ## Examples diff --git a/derive/src/initial.rs b/derive/src/initial.rs deleted file mode 100644 index ced4032..0000000 --- a/derive/src/initial.rs +++ /dev/null @@ -1,136 +0,0 @@ -// For the full copyright and license information, please view the LICENSE -// file that was distributed with this source code. - -use syn::{ - parse::{Parse, ParseStream}, - parse_macro_input, Data, DeriveInput, Fields, Token, -}; - -use proc_macro::TokenStream; -use quote::quote; -use syn::{punctuated::Punctuated, Attribute, Expr, LitStr}; - -mod kw { - syn::custom_keyword!(env); -} - -enum InitialArg { - Expr(Expr), - Env(String), -} - -#[derive(Default)] -struct InitialField { - expr: Option, - env: Option, -} - -impl Parse for InitialArg { - fn parse(input: ParseStream) -> syn::Result { - if input.peek(kw::env) && input.peek2(Token![=]) { - input.parse::()?; - input.parse::()?; - Ok(InitialArg::Env(input.parse::()?.value())) - } else { - Ok(InitialArg::Expr(input.parse::()?)) - } - } -} - -impl InitialField { - fn from_attribute(attribute: &Attribute) -> syn::Result { - let mut _self = Self::default(); - - let args = - attribute.parse_args_with(Punctuated::::parse_terminated)?; - - for arg in args { - match arg { - InitialArg::Expr(e) => { - if _self.expr.is_some() { - panic!("Can only specify one initial expression") - } - _self.expr = Some(e); - } - InitialArg::Env(s) => { - if _self.expr.is_some() { - panic!("Can only specify one env variable") - } - _self.env = Some(s); - } - } - } - - Ok(_self) - } - - fn into_expr(self) -> proc_macro2::TokenStream { - let mut default_value = match self.expr { - Some(val) => quote!(#val), - None => quote!(::core::default::Default::default()), - }; - - if let Some(env_var) = self.env { - default_value = quote!( - ::std::env::var_os(#env_var) - .and_then(|v| ::uutils_args::Value::from_value(&v).ok()) - .unwrap_or(#default_value) - ); - } - - default_value - } -} - -pub fn initial(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - - let name = input.ident; - let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); - - let function_body = match input.data { - Data::Struct(data) => initial_struct(data), - _ => panic!("Initial derive macro can only be used on structs"), - }; - - quote!( - impl #impl_generics Initial for #name #ty_generics #where_clause { - fn initial() -> Self { - #function_body - } - } - ) - .into() -} - -fn initial_struct(data: syn::DataStruct) -> proc_macro2::TokenStream { - let Fields::Named(fields) = data.fields else { - panic!("Fields must be named"); - }; - - // The key of this map is a literal pattern and the value - // is whatever code needs to be run when that pattern is encountered. - let mut defaults = Vec::new(); - for field in fields.named { - let ident = field.ident; - let field = parse_field_attr(&field.attrs); - let default_value = field.into_expr(); - - defaults.push(quote!(#ident: #default_value)); - } - - quote!( - Self { - #(#defaults),* - } - ) -} - -fn parse_field_attr(attrs: &[Attribute]) -> InitialField { - for attr in attrs { - if attr.path().is_ident("initial") { - return InitialField::from_attribute(attr).expect("Failed to parse initial attribute"); - } - } - InitialField::default() -} diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 7383602..fc463bf 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -6,7 +6,6 @@ mod attributes; mod flags; mod help; mod help_parser; -mod initial; use argument::{ free_handling, long_handling, parse_argument, parse_arguments_attr, positional_handling, @@ -19,11 +18,6 @@ use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, Data::Enum, DeriveInput}; -#[proc_macro_derive(Initial, attributes(initial))] -pub fn initial(input: TokenStream) -> TokenStream { - initial::initial(input) -} - #[proc_macro_derive(Arguments, attributes(arg, arguments))] pub fn arguments(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); diff --git a/examples/deprecated.rs b/examples/deprecated.rs index ad92a42..3e68ba2 100644 --- a/examples/deprecated.rs +++ b/examples/deprecated.rs @@ -1,4 +1,4 @@ -use uutils_args::{Arguments, Initial, Options}; +use uutils_args::{Arguments, Options}; fn parse_minus(s: &str) -> Option<&str> { let num = s.strip_prefix('-')?; @@ -27,7 +27,7 @@ enum Arg { Plus(isize), } -#[derive(Initial)] +#[derive(Default)] struct Settings { n1: usize, n2: isize, @@ -43,8 +43,8 @@ impl Options for Settings { } fn main() { - assert_eq!(Settings::parse(["test", "-10"]).n1, 10usize); - assert!(Settings::try_parse(["test", "--10"]).is_err()); - assert_eq!(Settings::parse(["test", "+10"]).n2, 10isize); - assert_eq!(Settings::parse(["test", "+-10"]).n2, -10isize); + assert_eq!(Settings::default().parse(["test", "-10"]).n1, 10usize); + assert!(Settings::default().try_parse(["test", "--10"]).is_err()); + assert_eq!(Settings::default().parse(["test", "+10"]).n2, 10isize); + assert_eq!(Settings::default().parse(["test", "+-10"]).n2, -10isize); } diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 5d95993..66137b4 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -1,4 +1,4 @@ -use uutils_args::{Arguments, Initial, Options}; +use uutils_args::{Arguments, Options}; #[derive(Arguments)] #[arguments(file = "examples/hello_world_help.md")] @@ -20,10 +20,8 @@ enum Arg { Hidden, } -#[derive(Initial)] struct Settings { name: String, - #[initial(1)] count: u8, } @@ -38,7 +36,12 @@ impl Options for Settings { } fn main() -> Result<(), uutils_args::Error> { - let settings = Settings::parse(std::env::args_os()); + let settings = Settings { + name: String::new(), + count: 1, + } + .parse(std::env::args_os()); + for _ in 0..settings.count { println!("Hello, {}!", settings.name); } diff --git a/src/lib.rs b/src/lib.rs index 72321c8..c9e2ee0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -160,21 +160,6 @@ impl ArgumentIter { } } -/// An alternative for the [`Default`] trait, with a more feature -/// packed derive macro. -/// -/// The `Initial` trait is used by `Options` to construct the initial -/// state of the options before any arguments are parsed. -/// -/// The [derive macro](derive@Initial) supports setting the initial -/// value per field and parsing the initial values from environment -/// variables. Otherwise, it will be equivalent to the derive macro -/// for the [`Default`] trait. -pub trait Initial: Sized { - /// Create the initial state of `Self` - fn initial() -> Self; -} - /// Defines the app settings by consuming [`Arguments`]. /// /// When implementing this trait, only two things need to be provided: @@ -184,35 +169,34 @@ pub trait Initial: Sized { /// type onto the options. /// /// By default, the [`Options::parse`] method will -/// 1. create a new instance of `Self` using [`Initial::initial`], -/// 2. repeatedly call [`ArgumentIter::next_arg`] and call [`Options::apply`] +/// 1. repeatedly call [`ArgumentIter::next_arg`] and call [`Options::apply`] /// on the result until the arguments are exhausted, -/// 3. and finally call [`Arguments::check_missing`]. -pub trait Options: Sized + Initial { +/// 2. and finally call [`Arguments::check_missing`] to check whether all +/// required arguments were given. +pub trait Options: Sized { /// Apply a single argument to the options. fn apply(&mut self, arg: Arg); - /// Parse an iterator of arguments into - fn parse(args: I) -> Self + /// Parse an iterator of arguments into the options + fn parse(self, args: I) -> Self where I: IntoIterator + 'static, I::Item: Into, { - exit_if_err(Self::try_parse(args), Arg::EXIT_CODE) + exit_if_err(self.try_parse(args), Arg::EXIT_CODE) } - fn try_parse(args: I) -> Result + fn try_parse(mut self, args: I) -> Result where I: IntoIterator + 'static, I::Item: Into, { - let mut _self = Self::initial(); let mut iter = Arg::parse(args); while let Some(arg) = iter.next_arg()? { - _self.apply(arg); + self.apply(arg); } Arg::check_missing(iter.positional_idx)?; - Ok(_self) + Ok(self) } } diff --git a/tests/coreutils/b2sum.rs b/tests/coreutils/b2sum.rs index d32a183..0fcfc81 100644 --- a/tests/coreutils/b2sum.rs +++ b/tests/coreutils/b2sum.rs @@ -1,5 +1,5 @@ use std::path::{Path, PathBuf}; -use uutils_args::{Arguments, Initial, Options}; +use uutils_args::{Arguments, Options}; #[derive(Clone, Arguments)] enum Arg { @@ -39,7 +39,7 @@ enum CheckOutput { Status, } -#[derive(Initial)] +#[derive(Default)] struct Settings { binary: bool, check: bool, @@ -67,48 +67,66 @@ impl Options for Settings { #[test] fn binary() { - assert!(!Settings::parse(["b2sum"]).binary); - assert!(!Settings::parse(["b2sum", "--text"]).binary); - assert!(!Settings::parse(["b2sum", "-t"]).binary); - assert!(!Settings::parse(["b2sum", "--binary", "--text"]).binary); - assert!(!Settings::parse(["b2sum", "-b", "-t"]).binary); - - assert!(Settings::parse(["b2sum", "--binary"]).binary); - assert!(Settings::parse(["b2sum", "-b"]).binary); - assert!(Settings::parse(["b2sum", "--text", "--binary"]).binary); - assert!(Settings::parse(["b2sum", "-t", "-b"]).binary); + assert!(!Settings::default().parse(["b2sum"]).binary); + assert!(!Settings::default().parse(["b2sum", "--text"]).binary); + assert!(!Settings::default().parse(["b2sum", "-t"]).binary); + assert!( + !Settings::default() + .parse(["b2sum", "--binary", "--text"]) + .binary + ); + assert!(!Settings::default().parse(["b2sum", "-b", "-t"]).binary); + + assert!(Settings::default().parse(["b2sum", "--binary"]).binary); + assert!(Settings::default().parse(["b2sum", "-b"]).binary); + assert!( + Settings::default() + .parse(["b2sum", "--text", "--binary"]) + .binary + ); + assert!(Settings::default().parse(["b2sum", "-t", "-b"]).binary); } #[test] fn check_output() { assert_eq!( - Settings::parse(["b2sum", "--warn"]).check_output, + Settings::default().parse(["b2sum", "--warn"]).check_output, CheckOutput::Warn ); assert_eq!( - Settings::parse(["b2sum", "--quiet"]).check_output, + Settings::default().parse(["b2sum", "--quiet"]).check_output, CheckOutput::Quiet ); assert_eq!( - Settings::parse(["b2sum", "--status"]).check_output, + Settings::default() + .parse(["b2sum", "--status"]) + .check_output, CheckOutput::Status ); assert_eq!( - Settings::parse(["b2sum", "--status", "--warn"]).check_output, + Settings::default() + .parse(["b2sum", "--status", "--warn"]) + .check_output, CheckOutput::Warn ); assert_eq!( - Settings::parse(["b2sum", "--status", "--warn"]).check_output, + Settings::default() + .parse(["b2sum", "--status", "--warn"]) + .check_output, CheckOutput::Warn ); assert_eq!( - Settings::parse(["b2sum", "--warn", "--quiet"]).check_output, + Settings::default() + .parse(["b2sum", "--warn", "--quiet"]) + .check_output, CheckOutput::Quiet ); assert_eq!( - Settings::parse(["b2sum", "--quiet", "--status"]).check_output, + Settings::default() + .parse(["b2sum", "--quiet", "--status"]) + .check_output, CheckOutput::Status ); } @@ -116,7 +134,7 @@ fn check_output() { #[test] fn files() { assert_eq!( - Settings::parse(["b2sum", "foo", "bar"]).files, + Settings::default().parse(["b2sum", "foo", "bar"]).files, vec![Path::new("foo"), Path::new("bar")] ); } diff --git a/tests/coreutils/base32.rs b/tests/coreutils/base32.rs index 6681daf..3ec67ad 100644 --- a/tests/coreutils/base32.rs +++ b/tests/coreutils/base32.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use uutils_args::{Arguments, Initial, Options}; +use uutils_args::{Arguments, Options}; #[derive(Clone, Arguments)] enum Arg { @@ -17,15 +17,24 @@ enum Arg { File(PathBuf), } -#[derive(Initial)] struct Settings { decode: bool, ignore_garbage: bool, - #[initial(Some(76))] wrap: Option, file: Option, } +impl Default for Settings { + fn default() -> Self { + Self { + wrap: Some(76), + decode: Default::default(), + ignore_garbage: Default::default(), + file: Default::default(), + } + } +} + impl Options for Settings { fn apply(&mut self, arg: Arg) { match arg { @@ -40,8 +49,14 @@ impl Options for Settings { #[test] fn wrap() { - assert_eq!(Settings::parse(["base32"]).wrap, Some(76)); - assert_eq!(Settings::parse(["base32", "-w0"]).wrap, None); - assert_eq!(Settings::parse(["base32", "-w100"]).wrap, Some(100)); - assert_eq!(Settings::parse(["base32", "--wrap=100"]).wrap, Some(100)); + assert_eq!(Settings::default().parse(["base32"]).wrap, Some(76)); + assert_eq!(Settings::default().parse(["base32", "-w0"]).wrap, None); + assert_eq!( + Settings::default().parse(["base32", "-w100"]).wrap, + Some(100) + ); + assert_eq!( + Settings::default().parse(["base32", "--wrap=100"]).wrap, + Some(100) + ); } diff --git a/tests/coreutils/basename.rs b/tests/coreutils/basename.rs index 404ad60..7ceaa33 100644 --- a/tests/coreutils/basename.rs +++ b/tests/coreutils/basename.rs @@ -1,4 +1,4 @@ -use uutils_args::{Arguments, Initial, Options}; +use uutils_args::{Arguments, Options}; #[derive(Clone, Arguments)] enum Arg { @@ -15,7 +15,7 @@ enum Arg { Names(Vec), } -#[derive(Initial)] +#[derive(Default)] struct Settings { multiple: bool, suffix: String, @@ -38,7 +38,7 @@ impl Options for Settings { } fn parse(args: &'static [&'static str]) -> Settings { - let mut settings = Settings::parse(args); + let mut settings = Settings::default().parse(args); if !settings.multiple { assert_eq!(settings.names.len(), 2); settings.suffix = settings.names.pop().unwrap(); diff --git a/tests/coreutils/cat.rs b/tests/coreutils/cat.rs index 5932f6c..b809b91 100644 --- a/tests/coreutils/cat.rs +++ b/tests/coreutils/cat.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use uutils_args::{Arguments, Initial, Options}; +use uutils_args::{Arguments, Options}; #[derive(Default)] enum NumberingMode { @@ -43,7 +43,7 @@ enum Arg { File(PathBuf), } -#[derive(Initial)] +#[derive(Default)] struct Settings { show_tabs: bool, show_ends: bool, @@ -82,27 +82,27 @@ impl Options for Settings { #[test] fn show() { - let s = Settings::parse(["cat", "-v"]); + let s = Settings::default().parse(["cat", "-v"]); assert!(!s.show_ends && !s.show_tabs && s.show_nonprinting); - let s = Settings::parse(["cat", "-E"]); + let s = Settings::default().parse(["cat", "-E"]); assert!(s.show_ends && !s.show_tabs && !s.show_nonprinting); - let s = Settings::parse(["cat", "-T"]); + let s = Settings::default().parse(["cat", "-T"]); assert!(!s.show_ends && s.show_tabs && !s.show_nonprinting); - let s = Settings::parse(["cat", "-e"]); + let s = Settings::default().parse(["cat", "-e"]); assert!(s.show_ends && !s.show_tabs && s.show_nonprinting); - let s = Settings::parse(["cat", "-t"]); + let s = Settings::default().parse(["cat", "-t"]); assert!(!s.show_ends && s.show_tabs && s.show_nonprinting); - let s = Settings::parse(["cat", "-A"]); + let s = Settings::default().parse(["cat", "-A"]); assert!(s.show_ends && s.show_tabs && s.show_nonprinting); - let s = Settings::parse(["cat", "-te"]); + let s = Settings::default().parse(["cat", "-te"]); assert!(s.show_ends && s.show_tabs && s.show_nonprinting); - let s = Settings::parse(["cat", "-vET"]); + let s = Settings::default().parse(["cat", "-vET"]); assert!(s.show_ends && s.show_tabs && s.show_nonprinting); } diff --git a/tests/coreutils/dd.rs b/tests/coreutils/dd.rs index ebac8f0..6d9c28a 100644 --- a/tests/coreutils/dd.rs +++ b/tests/coreutils/dd.rs @@ -1,7 +1,7 @@ // spell-checker:ignore noxfer infile outfile iseek oseek conv iflag oflag iflags oflags use std::path::PathBuf; -use uutils_args::{Arguments, Initial, Options, Value}; +use uutils_args::{Arguments, Options, Value}; #[derive(Value, Debug, PartialEq, Eq)] enum StatusLevel { @@ -56,13 +56,11 @@ enum Arg { Oflag(String), } -#[derive(Initial, Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq)] struct Settings { infile: Option, outfile: Option, - #[initial(512)] ibs: usize, - #[initial(512)] obs: usize, skip: u64, seek: u64, @@ -74,6 +72,25 @@ struct Settings { status: Option, } +impl Default for Settings { + fn default() -> Self { + Self { + ibs: 512, + obs: 512, + infile: Default::default(), + outfile: Default::default(), + skip: Default::default(), + seek: Default::default(), + count: Default::default(), + _iconv: Default::default(), + _iflags: Default::default(), + _oconv: Default::default(), + _oflags: Default::default(), + status: Default::default(), + } + } +} + impl Options for Settings { fn apply(&mut self, arg: Arg) { match arg { @@ -99,16 +116,19 @@ impl Options for Settings { #[test] fn empty() { - assert_eq!(Settings::try_parse(["dd"]).unwrap(), Settings::initial()) + assert_eq!( + Settings::default().try_parse(["dd"]).unwrap(), + Settings::default() + ) } #[test] fn infile() { assert_eq!( - Settings::try_parse(["dd", "if=hello"]).unwrap(), + Settings::default().try_parse(["dd", "if=hello"]).unwrap(), Settings { infile: Some(PathBuf::from("hello")), - ..Settings::initial() + ..Settings::default() } ) } @@ -116,10 +136,10 @@ fn infile() { #[test] fn outfile() { assert_eq!( - Settings::try_parse(["dd", "of=hello"]).unwrap(), + Settings::default().try_parse(["dd", "of=hello"]).unwrap(), Settings { outfile: Some(PathBuf::from("hello")), - ..Settings::initial() + ..Settings::default() } ) } @@ -127,35 +147,39 @@ fn outfile() { #[test] fn bs() { assert_eq!( - Settings::try_parse(["dd", "ibs=1"]).unwrap(), + Settings::default().try_parse(["dd", "ibs=1"]).unwrap(), Settings { ibs: 1, obs: 512, - ..Settings::initial() + ..Settings::default() } ); assert_eq!( - Settings::try_parse(["dd", "obs=1"]).unwrap(), + Settings::default().try_parse(["dd", "obs=1"]).unwrap(), Settings { ibs: 512, obs: 1, - ..Settings::initial() + ..Settings::default() } ); assert_eq!( - Settings::try_parse(["dd", "ibs=10", "obs=1"]).unwrap(), + Settings::default() + .try_parse(["dd", "ibs=10", "obs=1"]) + .unwrap(), Settings { ibs: 10, obs: 1, - ..Settings::initial() + ..Settings::default() } ); assert_eq!( - Settings::try_parse(["dd", "ibs=10", "bs=1"]).unwrap(), + Settings::default() + .try_parse(["dd", "ibs=10", "bs=1"]) + .unwrap(), Settings { ibs: 1, obs: 1, - ..Settings::initial() + ..Settings::default() } ) } diff --git a/tests/coreutils/echo.rs b/tests/coreutils/echo.rs index 2cc1753..69f482e 100644 --- a/tests/coreutils/echo.rs +++ b/tests/coreutils/echo.rs @@ -1,5 +1,5 @@ use std::ffi::OsString; -use uutils_args::{Arguments, Initial, Options}; +use uutils_args::{Arguments, Options}; #[derive(Arguments)] #[arguments(parse_echo_style)] @@ -20,7 +20,7 @@ enum Arg { String(Vec), } -#[derive(Initial)] +#[derive(Default)] struct Settings { trailing_newline: bool, escape: bool, @@ -43,16 +43,16 @@ impl Options for Settings { #[test] fn double_hyphen() { - let s = Settings::parse(["echo", "--"]); + let s = Settings::default().parse(["echo", "--"]); assert_eq!(s.strings, vec![OsString::from("--")]); - let s = Settings::parse(["echo", "--", "-n"]); + let s = Settings::default().parse(["echo", "--", "-n"]); assert_eq!(s.strings, vec![OsString::from("--"), OsString::from("-n")]); } #[test] #[ignore] fn nonexistent_options_are_values() { - let s = Settings::parse(["echo", "-f"]); + let s = Settings::default().parse(["echo", "-f"]); assert_eq!(s.strings, vec![OsString::from("-f")]); } diff --git a/tests/coreutils/head.rs b/tests/coreutils/head.rs index abe8ad4..40f3e81 100644 --- a/tests/coreutils/head.rs +++ b/tests/coreutils/head.rs @@ -1,6 +1,6 @@ use std::{ffi::OsString, path::PathBuf}; -use uutils_args::{Arguments, Initial, Options, Value}; +use uutils_args::{Arguments, Options, Value}; // This format is way to specific to implement using a library. Basically, any // deviation should be return `None` to indicate that we're not using the @@ -179,7 +179,7 @@ pub enum Mode { Lines, } -#[derive(Initial)] +#[derive(Default)] struct Settings { mode: Mode, number: SigNum, @@ -215,7 +215,7 @@ where { match parse_deprecated(iter.clone()) { Some(s) => Ok(s), - None => Settings::try_parse(iter), + None => Settings::default().try_parse(iter), } } diff --git a/tests/coreutils/ls.rs b/tests/coreutils/ls.rs index 75f3a9f..4e3e6de 100644 --- a/tests/coreutils/ls.rs +++ b/tests/coreutils/ls.rs @@ -1,5 +1,5 @@ use std::path::PathBuf; -use uutils_args::{Arguments, Initial, Options, Value}; +use uutils_args::{Arguments, Options, Value}; #[derive(Default, Debug, PartialEq, Eq, Value)] enum Format { @@ -302,7 +302,7 @@ fn default_terminal_size() -> u16 { 80 } -#[derive(Initial, Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq)] struct Settings { format: Format, files: Vec, @@ -322,20 +322,49 @@ struct Settings { long_numeric_uid_gid: bool, // alloc_size: bool, // block_size: Option, - #[initial(default_terminal_size())] width: u16, quoting_style: QuotingStyle, indicator_style: IndicatorStyle, // time_style: TimeStyle, context: bool, group_directories_first: bool, - #[initial('\n')] eol: char, which_files: Files, ignore_backups: bool, hide_control_chars: bool, } +impl Default for Settings { + fn default() -> Self { + Self { + eol: '\n', + width: default_terminal_size(), + format: Default::default(), + files: Default::default(), + sort: Default::default(), + recursive: Default::default(), + reverse: Default::default(), + dereference: Default::default(), + ignore_patterns: Default::default(), + directory: Default::default(), + time: Default::default(), + inode: Default::default(), + color: Default::default(), + long_author: Default::default(), + long_no_group: Default::default(), + long_no_owner: Default::default(), + long_numeric_uid_gid: Default::default(), + quoting_style: Default::default(), + indicator_style: Default::default(), + context: Default::default(), + group_directories_first: Default::default(), + which_files: Default::default(), + ignore_backups: Default::default(), + hide_control_chars: Default::default(), + } + } +} + impl Options for Settings { fn apply(&mut self, arg: Arg) { match arg { @@ -401,7 +430,7 @@ impl Options for Settings { #[test] fn default() { assert_eq!( - Settings::parse(["ls"]), + Settings::default().parse(["ls"]), Settings { format: Format::Columns, files: Vec::new(), @@ -433,87 +462,87 @@ fn default() { #[test] fn color() { - let s = Settings::parse(["ls", "--color"]); + let s = Settings::default().parse(["ls", "--color"]); assert!(s.color); - let s = Settings::parse(["ls", "--color=always"]); + let s = Settings::default().parse(["ls", "--color=always"]); assert!(s.color); - let s = Settings::parse(["ls", "--color=never"]); + let s = Settings::default().parse(["ls", "--color=never"]); assert!(!s.color); } #[test] fn format() { - let s = Settings::parse(["ls", "-l"]); + let s = Settings::default().parse(["ls", "-l"]); assert_eq!(s.format, Format::Long); - let s = Settings::parse(["ls", "-m"]); + let s = Settings::default().parse(["ls", "-m"]); assert_eq!(s.format, Format::Commas); - let s = Settings::parse(["ls", "--format=across"]); + let s = Settings::default().parse(["ls", "--format=across"]); assert_eq!(s.format, Format::Across); - let s = Settings::parse(["ls", "--format=acr"]); + let s = Settings::default().parse(["ls", "--format=acr"]); assert_eq!(s.format, Format::Across); - let s = Settings::parse(["ls", "-o"]); + let s = Settings::default().parse(["ls", "-o"]); assert_eq!(s.format, Format::Long); assert!(s.long_no_group && !s.long_no_owner && !s.long_numeric_uid_gid); - let s = Settings::parse(["ls", "-g"]); + let s = Settings::default().parse(["ls", "-g"]); assert_eq!(s.format, Format::Long); assert!(!s.long_no_group && s.long_no_owner && !s.long_numeric_uid_gid); - let s = Settings::parse(["ls", "-n"]); + let s = Settings::default().parse(["ls", "-n"]); assert_eq!(s.format, Format::Long); assert!(!s.long_no_group && !s.long_no_owner && s.long_numeric_uid_gid); - let s = Settings::parse(["ls", "-og"]); + let s = Settings::default().parse(["ls", "-og"]); assert_eq!(s.format, Format::Long); assert!(s.long_no_group && s.long_no_owner && !s.long_numeric_uid_gid); - let s = Settings::parse(["ls", "-on"]); + let s = Settings::default().parse(["ls", "-on"]); assert_eq!(s.format, Format::Long); assert!(s.long_no_group && !s.long_no_owner && s.long_numeric_uid_gid); - let s = Settings::parse(["ls", "-onCl"]); + let s = Settings::default().parse(["ls", "-onCl"]); assert_eq!(s.format, Format::Long); assert!(s.long_no_group && !s.long_no_owner && s.long_numeric_uid_gid); } #[test] fn time() { - let s = Settings::parse(["ls", "--time=access"]); + let s = Settings::default().parse(["ls", "--time=access"]); assert_eq!(s.time, Time::Access); - let s = Settings::parse(["ls", "--time=a"]); + let s = Settings::default().parse(["ls", "--time=a"]); assert_eq!(s.time, Time::Access); } #[test] fn classify() { - let s = Settings::parse(["ls", "--indicator-style=classify"]); + let s = Settings::default().parse(["ls", "--indicator-style=classify"]); assert_eq!(s.indicator_style, IndicatorStyle::Classify); - let s = Settings::parse(["ls", "--classify"]); + let s = Settings::default().parse(["ls", "--classify"]); assert_eq!(s.indicator_style, IndicatorStyle::Classify); - let s = Settings::parse(["ls", "--classify=always"]); + let s = Settings::default().parse(["ls", "--classify=always"]); assert_eq!(s.indicator_style, IndicatorStyle::Classify); - let s = Settings::parse(["ls", "--classify=none"]); + let s = Settings::default().parse(["ls", "--classify=none"]); assert_eq!(s.indicator_style, IndicatorStyle::None); - let s = Settings::parse(["ls", "-F"]); + let s = Settings::default().parse(["ls", "-F"]); assert_eq!(s.indicator_style, IndicatorStyle::Classify); } #[test] fn sort() { - let s = Settings::parse(["ls", "--sort=time"]); + let s = Settings::default().parse(["ls", "--sort=time"]); assert_eq!(s.sort, Sort::Time); - let s = Settings::parse(["ls", "-X"]); + let s = Settings::default().parse(["ls", "-X"]); assert_eq!(s.sort, Sort::Extension); } diff --git a/tests/coreutils/mktemp.rs b/tests/coreutils/mktemp.rs index ad18b74..c713eef 100644 --- a/tests/coreutils/mktemp.rs +++ b/tests/coreutils/mktemp.rs @@ -1,6 +1,6 @@ use std::path::{Path, PathBuf}; -use uutils_args::{Arguments, Initial, Options}; +use uutils_args::{Arguments, Options}; #[derive(Clone, Arguments)] enum Arg { @@ -26,7 +26,7 @@ enum Arg { Template(String), } -#[derive(Default, Initial)] +#[derive(Default)] struct Settings { directory: bool, dry_run: bool, @@ -53,35 +53,35 @@ impl Options for Settings { #[test] fn suffix() { - let s = Settings::parse(["mktemp", "--suffix=hello"]); + let s = Settings::default().parse(["mktemp", "--suffix=hello"]); assert_eq!(s.suffix.unwrap(), "hello"); - let s = Settings::parse(["mktemp", "--suffix="]); + let s = Settings::default().parse(["mktemp", "--suffix="]); assert_eq!(s.suffix.unwrap(), ""); - let s = Settings::parse(["mktemp", "--suffix="]); + let s = Settings::default().parse(["mktemp", "--suffix="]); assert_eq!(s.suffix.unwrap(), ""); - let s = Settings::parse(["mktemp"]); + let s = Settings::default().parse(["mktemp"]); assert_eq!(s.suffix, None); } #[test] fn tmpdir() { - let s = Settings::parse(["mktemp", "--tmpdir"]); + let s = Settings::default().parse(["mktemp", "--tmpdir"]); assert_eq!(s.tmp_dir.unwrap(), Path::new(".")); - let s = Settings::parse(["mktemp", "--tmpdir="]); + let s = Settings::default().parse(["mktemp", "--tmpdir="]); assert_eq!(s.tmp_dir.unwrap(), Path::new("")); - let s = Settings::parse(["mktemp", "-p", "foo"]); + let s = Settings::default().parse(["mktemp", "-p", "foo"]); assert_eq!(s.tmp_dir.unwrap(), Path::new("foo")); - let s = Settings::parse(["mktemp", "-pfoo"]); + let s = Settings::default().parse(["mktemp", "-pfoo"]); assert_eq!(s.tmp_dir.unwrap(), Path::new("foo")); - let s = Settings::parse(["mktemp", "-p", ""]); + let s = Settings::default().parse(["mktemp", "-p", ""]); assert_eq!(s.tmp_dir.unwrap(), Path::new("")); - assert!(Settings::try_parse(["mktemp", "-p"]).is_err()); + assert!(Settings::default().try_parse(["mktemp", "-p"]).is_err()); } diff --git a/tests/coreutils/tail.rs b/tests/coreutils/tail.rs index c23ba7b..31cfda9 100644 --- a/tests/coreutils/tail.rs +++ b/tests/coreutils/tail.rs @@ -1,6 +1,6 @@ use std::{ffi::OsString, path::PathBuf}; -use uutils_args::{Arguments, Initial, Options, Value}; +use uutils_args::{Arguments, Options, Value}; // This format is way to specific to implement using a library. Basically, any // deviation should be return `None` to indicate that we're not using the @@ -100,7 +100,7 @@ where mode, follow, inputs: vec![input.into().into()], - ..Settings::initial() + ..Settings::default() }) } @@ -228,7 +228,7 @@ pub enum Mode { Lines, } -#[derive(Initial)] +#[derive(Default)] struct Settings { follow: Option, max_unchanged_stats: u32, @@ -280,7 +280,7 @@ where { match parse_deprecated(iter.clone()) { Some(s) => Ok(s), - None => Settings::try_parse(iter), + None => Settings::default().try_parse(iter), } } diff --git a/tests/coreutils/uniq.rs b/tests/coreutils/uniq.rs index bf94e76..b8eb7e4 100644 --- a/tests/coreutils/uniq.rs +++ b/tests/coreutils/uniq.rs @@ -50,7 +50,7 @@ enum Delimiters { Both, } -#[derive(Initial)] +#[derive(Default)] struct Settings { repeats_only: bool, uniques_only: bool, diff --git a/tests/defaults.rs b/tests/defaults.rs deleted file mode 100644 index 76a6511..0000000 --- a/tests/defaults.rs +++ /dev/null @@ -1,55 +0,0 @@ -use uutils_args::{Arguments, Initial, Options}; - -#[test] -fn true_default() { - #[derive(Arguments)] - enum Arg { - #[arg("--foo")] - Foo, - } - - #[derive(Initial)] - struct Settings { - #[initial(true)] - foo: bool, - } - - impl Options for Settings { - fn apply(&mut self, Arg::Foo: Arg) { - self.foo = false; - } - } - - assert!(Settings::parse(["test"]).foo); - assert!(!Settings::parse(["test", "--foo"]).foo); -} - -#[test] -fn env_var_string() { - #[derive(Arguments)] - enum Arg { - #[arg("--foo=MSG")] - Foo(String), - } - - #[derive(Initial)] - struct Settings { - #[initial(env = "FOO")] - foo: String, - } - - impl Options for Settings { - fn apply(&mut self, Arg::Foo(x): Arg) { - self.foo = x; - } - } - - std::env::set_var("FOO", "one"); - assert_eq!(Settings::parse(["test"]).foo, "one"); - - std::env::set_var("FOO", "two"); - assert_eq!(Settings::parse(["test"]).foo, "two"); - - std::env::remove_var("FOO"); - assert_eq!(Settings::parse(["test"]).foo, ""); -} diff --git a/tests/flags.rs b/tests/flags.rs index 2a6b808..1c4bd64 100644 --- a/tests/flags.rs +++ b/tests/flags.rs @@ -1,4 +1,4 @@ -use uutils_args::{Arguments, Initial, Options}; +use uutils_args::{Arguments, Options}; #[test] fn one_flag() { @@ -8,7 +8,7 @@ fn one_flag() { Foo, } - #[derive(Initial)] + #[derive(Default)] struct Settings { foo: bool, } @@ -21,7 +21,7 @@ fn one_flag() { } } - let settings = Settings::parse(["test", "-f"]); + let settings = Settings::default().parse(["test", "-f"]); assert!(settings.foo); } @@ -35,7 +35,7 @@ fn two_flags() { B, } - #[derive(Initial, PartialEq, Eq, Debug)] + #[derive(Default, PartialEq, Eq, Debug)] struct Settings { a: bool, b: bool, @@ -51,16 +51,19 @@ fn two_flags() { } assert_eq!( - Settings::parse(["test", "-a"]), + Settings::default().parse(["test", "-a"]), Settings { a: true, b: false } ); - assert_eq!(Settings::parse(["test"]), Settings { a: false, b: false }); assert_eq!( - Settings::parse(["test", "-b"]), + Settings::default().parse(["test"]), + Settings { a: false, b: false } + ); + assert_eq!( + Settings::default().parse(["test", "-b"]), Settings { a: false, b: true } ); assert_eq!( - Settings::parse(["test", "-a", "-b"]), + Settings::default().parse(["test", "-a", "-b"]), Settings { a: true, b: true } ); } @@ -73,7 +76,7 @@ fn long_and_short_flag() { Foo, } - #[derive(Initial)] + #[derive(Default)] struct Settings { foo: bool, } @@ -84,9 +87,9 @@ fn long_and_short_flag() { } } - assert!(!Settings::parse(["test"]).foo); - assert!(Settings::parse(["test", "--foo"]).foo); - assert!(Settings::parse(["test", "-f"]).foo); + assert!(!Settings::default().parse(["test"]).foo); + assert!(Settings::default().parse(["test", "--foo"]).foo); + assert!(Settings::default().parse(["test", "-f"]).foo); } #[test] @@ -97,7 +100,7 @@ fn short_alias() { Foo, } - #[derive(Initial)] + #[derive(Default)] struct Settings { foo: bool, } @@ -108,7 +111,7 @@ fn short_alias() { } } - assert!(Settings::parse(["test", "-b"]).foo); + assert!(Settings::default().parse(["test", "-b"]).foo); } #[test] @@ -119,7 +122,7 @@ fn long_alias() { Foo, } - #[derive(Initial)] + #[derive(Default)] struct Settings { foo: bool, } @@ -130,7 +133,7 @@ fn long_alias() { } } - assert!(Settings::parse(["test", "--bar"]).foo); + assert!(Settings::default().parse(["test", "--bar"]).foo); } #[test] @@ -143,7 +146,7 @@ fn short_and_long_alias() { Bar, } - #[derive(Initial, PartialEq, Eq, Debug)] + #[derive(Default, PartialEq, Eq, Debug)] struct Settings { foo: bool, bar: bool, @@ -168,10 +171,10 @@ fn short_and_long_alias() { bar: true, }; - assert_eq!(Settings::parse(["test", "--bar"]), foo_true); - assert_eq!(Settings::parse(["test", "-b"]), foo_true); - assert_eq!(Settings::parse(["test", "--foo"]), bar_true); - assert_eq!(Settings::parse(["test", "-f"]), bar_true); + assert_eq!(Settings::default().parse(["test", "--bar"]), foo_true); + assert_eq!(Settings::default().parse(["test", "-b"]), foo_true); + assert_eq!(Settings::default().parse(["test", "--foo"]), bar_true); + assert_eq!(Settings::default().parse(["test", "-f"]), bar_true); } #[test] @@ -186,7 +189,7 @@ fn xyz_map_to_abc() { Z, } - #[derive(Initial, PartialEq, Eq, Debug)] + #[derive(Default, PartialEq, Eq, Debug)] struct Settings { a: bool, b: bool, @@ -214,7 +217,7 @@ fn xyz_map_to_abc() { } assert_eq!( - Settings::parse(["test", "-x"]), + Settings::default().parse(["test", "-x"]), Settings { a: true, b: true, @@ -223,7 +226,7 @@ fn xyz_map_to_abc() { ); assert_eq!( - Settings::parse(["test", "-y"]), + Settings::default().parse(["test", "-y"]), Settings { a: false, b: true, @@ -232,7 +235,7 @@ fn xyz_map_to_abc() { ); assert_eq!( - Settings::parse(["test", "-xy"]), + Settings::default().parse(["test", "-xy"]), Settings { a: true, b: true, @@ -241,7 +244,7 @@ fn xyz_map_to_abc() { ); assert_eq!( - Settings::parse(["test", "-z"]), + Settings::default().parse(["test", "-z"]), Settings { a: true, b: true, @@ -260,7 +263,7 @@ fn non_rust_ident() { Super, } - #[derive(Initial, PartialEq, Eq, Debug)] + #[derive(Default, PartialEq, Eq, Debug)] struct Settings { a: bool, b: bool, @@ -276,7 +279,7 @@ fn non_rust_ident() { } assert_eq!( - Settings::parse(["test", "--foo-bar", "--super"]), + Settings::default().parse(["test", "--foo-bar", "--super"]), Settings { a: true, b: true } ) } @@ -288,7 +291,7 @@ fn number_flag() { #[arg("-1")] One, } - #[derive(Initial, PartialEq, Eq, Debug)] + #[derive(Default, PartialEq, Eq, Debug)] struct Settings { one: bool, } @@ -299,7 +302,7 @@ fn number_flag() { } } - assert!(Settings::parse(["test", "-1"]).one) + assert!(Settings::default().parse(["test", "-1"]).one) } #[test] @@ -312,7 +315,7 @@ fn false_bool() { B, } - #[derive(Initial)] + #[derive(Default)] struct Settings { foo: bool, } @@ -326,12 +329,12 @@ fn false_bool() { } } - assert!(Settings::parse(["test", "-a"]).foo); - assert!(!Settings::parse(["test", "-b"]).foo); - assert!(!Settings::parse(["test", "-ab"]).foo); - assert!(Settings::parse(["test", "-ba"]).foo); - assert!(!Settings::parse(["test", "-a", "-b"]).foo); - assert!(Settings::parse(["test", "-b", "-a"]).foo); + assert!(Settings::default().parse(["test", "-a"]).foo); + assert!(!Settings::default().parse(["test", "-b"]).foo); + assert!(!Settings::default().parse(["test", "-ab"]).foo); + assert!(Settings::default().parse(["test", "-ba"]).foo); + assert!(!Settings::default().parse(["test", "-a", "-b"]).foo); + assert!(Settings::default().parse(["test", "-b", "-a"]).foo); } #[test] @@ -342,7 +345,7 @@ fn verbosity() { Verbosity, } - #[derive(Initial)] + #[derive(Default)] struct Settings { verbosity: u8, } @@ -353,9 +356,9 @@ fn verbosity() { } } - assert_eq!(Settings::parse(["test", "-v"]).verbosity, 1); - assert_eq!(Settings::parse(["test", "-vv"]).verbosity, 2); - assert_eq!(Settings::parse(["test", "-vvv"]).verbosity, 3); + assert_eq!(Settings::default().parse(["test", "-v"]).verbosity, 1); + assert_eq!(Settings::default().parse(["test", "-vv"]).verbosity, 2); + assert_eq!(Settings::default().parse(["test", "-vvv"]).verbosity, 3); } #[test] @@ -370,7 +373,7 @@ fn infer_long_args() { Author, } - #[derive(Initial)] + #[derive(Default)] struct Settings { all: bool, almost_all: bool, @@ -387,10 +390,10 @@ fn infer_long_args() { } } - assert!(Settings::parse(["test", "--all"]).all); - assert!(Settings::parse(["test", "--alm"]).almost_all); - assert!(Settings::parse(["test", "--au"]).author); - assert!(Settings::try_parse(["test", "--a"]).is_err()); + assert!(Settings::default().parse(["test", "--all"]).all); + assert!(Settings::default().parse(["test", "--alm"]).almost_all); + assert!(Settings::default().parse(["test", "--au"]).author); + assert!(Settings::default().try_parse(["test", "--a"]).is_err()); } #[test] @@ -413,7 +416,7 @@ fn enum_flag() { Baz, } - #[derive(Initial)] + #[derive(Default)] struct Settings { foo: SomeEnum, } @@ -428,7 +431,13 @@ fn enum_flag() { } } - assert_eq!(Settings::parse(["test"]).foo, SomeEnum::Foo); - assert_eq!(Settings::parse(["test", "--bar"]).foo, SomeEnum::Bar); - assert_eq!(Settings::parse(["test", "--baz"]).foo, SomeEnum::Baz,); + assert_eq!(Settings::default().parse(["test"]).foo, SomeEnum::Foo); + assert_eq!( + Settings::default().parse(["test", "--bar"]).foo, + SomeEnum::Bar + ); + assert_eq!( + Settings::default().parse(["test", "--baz"]).foo, + SomeEnum::Baz, + ); } diff --git a/tests/options.rs b/tests/options.rs index ba02512..729a107 100644 --- a/tests/options.rs +++ b/tests/options.rs @@ -1,6 +1,6 @@ use std::ffi::OsStr; -use uutils_args::{Arguments, Initial, Options, Value, ValueResult}; +use uutils_args::{Arguments, Options, Value, ValueResult}; #[test] fn string_option() { @@ -10,7 +10,7 @@ fn string_option() { Message(String), } - #[derive(Initial)] + #[derive(Default)] struct Settings { message: String, } @@ -22,7 +22,9 @@ fn string_option() { } assert_eq!( - Settings::parse(["test", "--message=hello"]).message, + Settings::default() + .parse(["test", "--message=hello"]) + .message, "hello" ); } @@ -46,7 +48,7 @@ fn enum_option() { Format(Format), } - #[derive(Initial)] + #[derive(Default)] struct Settings { format: Format, } @@ -58,12 +60,14 @@ fn enum_option() { } assert_eq!( - Settings::parse(["test", "--format=bar"]).format, + Settings::default().parse(["test", "--format=bar"]).format, Format::Bar ); assert_eq!( - Settings::parse(["test", "--format", "baz"]).format, + Settings::default() + .parse(["test", "--format", "baz"]) + .format, Format::Baz ); } @@ -85,7 +89,7 @@ fn enum_option_with_fields() { Indent(Indent), } - #[derive(Initial)] + #[derive(Default)] struct Settings { indent: Indent, } @@ -97,11 +101,11 @@ fn enum_option_with_fields() { } assert_eq!( - Settings::parse(["test", "-i=thin"]).indent, + Settings::default().parse(["test", "-i=thin"]).indent, Indent::Spaces(4) ); assert_eq!( - Settings::parse(["test", "-i=wide"]).indent, + Settings::default().parse(["test", "-i=wide"]).indent, Indent::Spaces(8) ); } @@ -134,7 +138,7 @@ fn enum_with_complex_from_value() { Indent(Indent), } - #[derive(Initial)] + #[derive(Default)] struct Settings { indent: Indent, } @@ -145,8 +149,14 @@ fn enum_with_complex_from_value() { } } - assert_eq!(Settings::parse(["test", "-i=tabs"]).indent, Indent::Tabs); - assert_eq!(Settings::parse(["test", "-i=4"]).indent, Indent::Spaces(4)); + assert_eq!( + Settings::default().parse(["test", "-i=tabs"]).indent, + Indent::Tabs + ); + assert_eq!( + Settings::default().parse(["test", "-i=4"]).indent, + Indent::Spaces(4) + ); } #[test] @@ -168,9 +178,8 @@ fn color() { Color(Option), } - #[derive(Initial)] + #[derive(Default)] struct Settings { - #[initial(Color::Auto)] color: Color, } @@ -181,20 +190,29 @@ fn color() { } assert_eq!( - Settings::parse(["test", "--color=yes"]).color, + Settings::default().parse(["test", "--color=yes"]).color, Color::Always ); assert_eq!( - Settings::parse(["test", "--color=always"]).color, + Settings::default().parse(["test", "--color=always"]).color, Color::Always ); - assert_eq!(Settings::parse(["test", "--color=no"]).color, Color::Never); assert_eq!( - Settings::parse(["test", "--color=never"]).color, + Settings::default().parse(["test", "--color=no"]).color, + Color::Never + ); + assert_eq!( + Settings::default().parse(["test", "--color=never"]).color, Color::Never ); - assert_eq!(Settings::parse(["test", "--color=auto"]).color, Color::Auto); - assert_eq!(Settings::parse(["test", "--color"]).color, Color::Always) + assert_eq!( + Settings::default().parse(["test", "--color=auto"]).color, + Color::Auto + ); + assert_eq!( + Settings::default().parse(["test", "--color"]).color, + Color::Always + ) } #[test] @@ -209,7 +227,7 @@ fn actions() { Receive, } - #[derive(Initial)] + #[derive(Default)] struct Settings { last_message: String, send: bool, @@ -229,7 +247,7 @@ fn actions() { } } - let settings = Settings::parse(["test", "-m=Hello", "-m=World", "--send"]); + let settings = Settings::default().parse(["test", "-m=Hello", "-m=World", "--send"]); assert_eq!(settings.messages, vec!["Hello", "World"]); assert_eq!(settings.last_message, "World"); assert!(settings.send); @@ -243,7 +261,7 @@ fn width() { Width(u64), } - #[derive(Initial)] + #[derive(Default)] struct Settings { width: Option, } @@ -257,8 +275,8 @@ fn width() { } } - assert_eq!(Settings::parse(["test", "-w=0"]).width, None); - assert_eq!(Settings::parse(["test", "-w=1"]).width, Some(1)); + assert_eq!(Settings::default().parse(["test", "-w=0"]).width, None); + assert_eq!(Settings::default().parse(["test", "-w=1"]).width, Some(1)); } #[test] @@ -287,7 +305,7 @@ fn integers() { I128(i128), } - #[derive(Initial)] + #[derive(Default)] struct Settings { n: i128, } @@ -309,17 +327,17 @@ fn integers() { } } - assert_eq!(Settings::parse(["test", "--u8=5"]).n, 5); - assert_eq!(Settings::parse(["test", "--u16=5"]).n, 5); - assert_eq!(Settings::parse(["test", "--u32=5"]).n, 5); - assert_eq!(Settings::parse(["test", "--u64=5"]).n, 5); - assert_eq!(Settings::parse(["test", "--u128=5"]).n, 5); + assert_eq!(Settings::default().parse(["test", "--u8=5"]).n, 5); + assert_eq!(Settings::default().parse(["test", "--u16=5"]).n, 5); + assert_eq!(Settings::default().parse(["test", "--u32=5"]).n, 5); + assert_eq!(Settings::default().parse(["test", "--u64=5"]).n, 5); + assert_eq!(Settings::default().parse(["test", "--u128=5"]).n, 5); - assert_eq!(Settings::parse(["test", "--i8=5"]).n, 5); - assert_eq!(Settings::parse(["test", "--i16=5"]).n, 5); - assert_eq!(Settings::parse(["test", "--i32=5"]).n, 5); - assert_eq!(Settings::parse(["test", "--i64=5"]).n, 5); - assert_eq!(Settings::parse(["test", "--i128=5"]).n, 5); + assert_eq!(Settings::default().parse(["test", "--i8=5"]).n, 5); + assert_eq!(Settings::default().parse(["test", "--i16=5"]).n, 5); + assert_eq!(Settings::default().parse(["test", "--i32=5"]).n, 5); + assert_eq!(Settings::default().parse(["test", "--i64=5"]).n, 5); + assert_eq!(Settings::default().parse(["test", "--i128=5"]).n, 5); } #[test] @@ -344,7 +362,7 @@ fn ls_classify() { Classify(When), } - #[derive(Initial)] + #[derive(Default)] struct Settings { classify: When, } @@ -355,17 +373,22 @@ fn ls_classify() { } } - assert_eq!(Settings::parse(["test"]).classify, When::Auto); + assert_eq!(Settings::default().parse(["test"]).classify, When::Auto); assert_eq!( - Settings::parse(["test", "--classify=never"]).classify, + Settings::default() + .parse(["test", "--classify=never"]) + .classify, When::Never, ); assert_eq!( - Settings::parse(["test", "--classify"]).classify, + Settings::default().parse(["test", "--classify"]).classify, + When::Always, + ); + assert_eq!( + Settings::default().parse(["test", "-F"]).classify, When::Always, ); - assert_eq!(Settings::parse(["test", "-F"]).classify, When::Always,); - assert!(Settings::try_parse(["test", "-Falways"]).is_err()); + assert!(Settings::default().try_parse(["test", "-Falways"]).is_err()); } #[test] @@ -379,7 +402,7 @@ fn mktemp_tmpdir() { TmpDir(String), } - #[derive(Initial)] + #[derive(Default)] struct Settings { tmpdir: Option, } @@ -390,16 +413,16 @@ fn mktemp_tmpdir() { } } - let settings = Settings::parse(["test", "-p", "X"]); + let settings = Settings::default().parse(["test", "-p", "X"]); assert_eq!(settings.tmpdir.unwrap(), "X"); - let settings = Settings::parse(["test", "--tmpdir=X"]); + let settings = Settings::default().parse(["test", "--tmpdir=X"]); assert_eq!(settings.tmpdir.unwrap(), "X"); - let settings = Settings::parse(["test", "--tmpdir"]); + let settings = Settings::default().parse(["test", "--tmpdir"]); assert_eq!(settings.tmpdir.unwrap(), "/tmp"); - assert!(Settings::try_parse(["test", "-p"]).is_err()); + assert!(Settings::default().try_parse(["test", "-p"]).is_err()); } #[test] @@ -452,7 +475,7 @@ fn deprecated() { Plus(isize), } - #[derive(Initial)] + #[derive(Default)] struct Settings { n1: usize, n2: isize, @@ -467,8 +490,8 @@ fn deprecated() { } } - assert_eq!(Settings::parse(["test", "-10"]).n1, 10usize); - assert!(Settings::try_parse(["test", "--10"]).is_err()); - assert_eq!(Settings::parse(["test", "+10"]).n2, 10isize); - assert_eq!(Settings::parse(["test", "+-10"]).n2, -10isize); + assert_eq!(Settings::default().parse(["test", "-10"]).n1, 10usize); + assert!(Settings::default().try_parse(["test", "--10"]).is_err()); + assert_eq!(Settings::default().parse(["test", "+10"]).n2, 10isize); + assert_eq!(Settings::default().parse(["test", "+-10"]).n2, -10isize); } diff --git a/tests/positionals.rs b/tests/positionals.rs index 04f187c..02eb1d4 100644 --- a/tests/positionals.rs +++ b/tests/positionals.rs @@ -1,4 +1,4 @@ -use uutils_args::{Arguments, Initial, Options}; +use uutils_args::{Arguments, Options}; #[test] fn one_positional() { @@ -8,7 +8,7 @@ fn one_positional() { File1(String), } - #[derive(Initial)] + #[derive(Default)] struct Settings { file1: String, } @@ -19,10 +19,10 @@ fn one_positional() { } } - let settings = Settings::parse(["test", "foo"]); + let settings = Settings::default().parse(["test", "foo"]); assert_eq!(settings.file1, "foo"); - assert!(Settings::try_parse(["test"]).is_err()); + assert!(Settings::default().try_parse(["test"]).is_err()); } #[test] @@ -35,7 +35,7 @@ fn two_positionals() { Bar(String), } - #[derive(Initial)] + #[derive(Default)] struct Settings { foo: String, bar: String, @@ -50,11 +50,11 @@ fn two_positionals() { } } - let settings = Settings::parse(["test", "a", "b"]); + let settings = Settings::default().parse(["test", "a", "b"]); assert_eq!(settings.foo, "a"); assert_eq!(settings.bar, "b"); - assert!(Settings::try_parse(["test"]).is_err()); + assert!(Settings::default().try_parse(["test"]).is_err()); } #[test] @@ -65,7 +65,7 @@ fn optional_positional() { Foo(String), } - #[derive(Initial)] + #[derive(Default)] struct Settings { foo: Option, } @@ -76,9 +76,9 @@ fn optional_positional() { } } - let settings = Settings::parse(["test"]); + let settings = Settings::default().parse(["test"]); assert_eq!(settings.foo, None); - let settings = Settings::parse(["test", "bar"]); + let settings = Settings::default().parse(["test", "bar"]); assert_eq!(settings.foo.unwrap(), "bar"); } @@ -90,7 +90,7 @@ fn collect_positional() { Foo(String), } - #[derive(Initial)] + #[derive(Default)] struct Settings { foo: Vec, } @@ -101,9 +101,9 @@ fn collect_positional() { } } - let settings = Settings::parse(["test", "a", "b", "c"]); + let settings = Settings::default().parse(["test", "a", "b", "c"]); assert_eq!(settings.foo, vec!["a", "b", "c"]); - let settings = Settings::parse(["test"]); + let settings = Settings::default().parse(["test"]); assert_eq!(settings.foo, Vec::::new()); } @@ -115,7 +115,7 @@ fn last1() { Foo(Vec), } - #[derive(Initial)] + #[derive(Default)] struct Settings { foo: Vec, } @@ -126,7 +126,7 @@ fn last1() { } } - let settings = Settings::parse(["test", "a", "-b", "c"]); + let settings = Settings::default().parse(["test", "a", "-b", "c"]); assert_eq!(settings.foo, vec!["a", "-b", "c"]); } @@ -141,7 +141,7 @@ fn last2() { Foo(Vec), } - #[derive(Initial)] + #[derive(Default)] struct Settings { foo: Vec, } @@ -155,9 +155,9 @@ fn last2() { } } - let settings = Settings::parse(["test", "-a"]); + let settings = Settings::default().parse(["test", "-a"]); assert_eq!(settings.foo, Vec::::new()); - let settings = Settings::parse(["test", "--", "-a"]); + let settings = Settings::default().parse(["test", "--", "-a"]); assert_eq!(settings.foo, vec!["-a"]); } From 19cf7bd4211fe5a791563132f40134016ed41944 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Thu, 7 Dec 2023 12:51:30 +0100 Subject: [PATCH 016/116] rename default to value --- derive/src/argument.rs | 2 +- derive/src/attributes.rs | 6 +++--- tests/coreutils/ls.rs | 34 +++++++++++++++++----------------- tests/coreutils/mktemp.rs | 2 +- tests/coreutils/tail.rs | 2 +- tests/options.rs | 4 ++-- 6 files changed, 25 insertions(+), 25 deletions(-) diff --git a/derive/src/argument.rs b/derive/src/argument.rs index 7e3928a..ee09c76 100644 --- a/derive/src/argument.rs +++ b/derive/src/argument.rs @@ -78,7 +78,7 @@ pub fn parse_argument(v: Variant) -> Vec { let mut arg_help = help.clone(); let arg_type = match attribute { ArgAttr::Option(opt) => { - let default_expr = match opt.default { + let default_expr = match opt.value { Some(expr) => quote!(#expr), None => quote!(Default::default()), }; diff --git a/derive/src/attributes.rs b/derive/src/attributes.rs index fc56dc7..1a77353 100644 --- a/derive/src/attributes.rs +++ b/derive/src/attributes.rs @@ -100,7 +100,7 @@ impl ArgAttr { pub struct OptionAttr { pub flags: Flags, pub parser: Option, - pub default: Option, + pub value: Option, pub hidden: bool, pub help: Option, } @@ -123,10 +123,10 @@ impl OptionAttr { let p = s.parse::()?; option_attr.parser = Some(p); } - "default" => { + "value" => { s.parse::()?; let d = s.parse::()?; - option_attr.default = Some(d); + option_attr.value = Some(d); } "hidden" => { option_attr.hidden = true; diff --git a/tests/coreutils/ls.rs b/tests/coreutils/ls.rs index 4e3e6de..1ebcfa4 100644 --- a/tests/coreutils/ls.rs +++ b/tests/coreutils/ls.rs @@ -146,17 +146,17 @@ enum Arg { Author, #[arg("--time=WORD")] - #[arg("-c", default = Time::Change)] - #[arg("-u", default = Time::Access)] + #[arg("-c", value = Time::Change)] + #[arg("-u", value = Time::Access)] Time(Time), // === Sorting == /// Sort by WORD #[arg("--sort=WORD")] - #[arg("-t", default = Sort::Time, help = "Sort by time")] - #[arg("-U", default = Sort::None, help = "Do not sort")] - #[arg("-v", default = Sort::Version, help = "Sort by version")] - #[arg("-X", default = Sort::Extension, help = "Sort by extension")] + #[arg("-t", value = Sort::Time, help = "Sort by time")] + #[arg("-U", value = Sort::None, help = "Do not sort")] + #[arg("-v", value = Sort::Version, help = "Sort by version")] + #[arg("-X", value = Sort::Extension, help = "Sort by extension")] Sort(Sort), // === Miscellaneous === @@ -200,10 +200,10 @@ enum Arg { // === Format === /// Set format #[arg("--format=FORMAT")] - #[arg("-l", "--long", default = Format::Long, help = "Use long format")] - #[arg("-C", default = Format::Columns, help = "Use columns format")] - #[arg("-x", default = Format::Across, help = "Use across format")] - #[arg("-m", default = Format::Commas, help = "Use comma format")] + #[arg("-l", "--long", value = Format::Long, help = "Use long format")] + #[arg("-C", value = Format::Columns, help = "Use columns format")] + #[arg("-x", value = Format::Across, help = "Use across format")] + #[arg("-m", value = Format::Commas, help = "Use comma format")] Format(Format), /// Show single column @@ -221,12 +221,12 @@ enum Arg { // === Indicator style === #[arg("--indicator-style=STYLE")] - #[arg("-p", default = IndicatorStyle::Slash, help = "Append slash to directories")] - #[arg("--file-type", default = IndicatorStyle::FileType, help = "Add indicators for file types")] + #[arg("-p", value = IndicatorStyle::Slash, help = "Append slash to directories")] + #[arg("--file-type", value = IndicatorStyle::FileType, help = "Add indicators for file types")] IndicatorStyle(IndicatorStyle), /// Classify items - #[arg("-F", "--classify[=WHEN]", default = When::Always)] + #[arg("-F", "--classify[=WHEN]", value = When::Always)] IndicatorStyleClassify(When), // === Dereference === @@ -254,13 +254,13 @@ enum Arg { // === Quoting style === #[arg("--quoting-style=STYLE")] - #[arg("-N", "--literal", default = QuotingStyle::Literal)] - #[arg("-h", "--escape", default = QuotingStyle::Escape)] - #[arg("-Q", "--quote-name", default = todo!())] + #[arg("-N", "--literal", value = QuotingStyle::Literal)] + #[arg("-h", "--escape", value = QuotingStyle::Escape)] + #[arg("-Q", "--quote-name", value = todo!())] QuotingStyle(QuotingStyle), /// Set the color - #[arg("--color[=WHEN]", default = When::Always)] + #[arg("--color[=WHEN]", value = When::Always)] Color(When), /// Print control characters as ? diff --git a/tests/coreutils/mktemp.rs b/tests/coreutils/mktemp.rs index c713eef..344cb06 100644 --- a/tests/coreutils/mktemp.rs +++ b/tests/coreutils/mktemp.rs @@ -19,7 +19,7 @@ enum Arg { #[arg("-t")] TreatAsTemplate, - #[arg("-p DIR", "--tmpdir[=DIR]", default = ".".into())] + #[arg("-p DIR", "--tmpdir[=DIR]", value = ".".into())] TmpDir(PathBuf), #[arg("TEMPLATE", 0..=1)] diff --git a/tests/coreutils/tail.rs b/tests/coreutils/tail.rs index 31cfda9..671b9be 100644 --- a/tests/coreutils/tail.rs +++ b/tests/coreutils/tail.rs @@ -109,7 +109,7 @@ enum Arg { #[arg("-c NUM", "--bytes=NUM")] Bytes(SigNum), - #[arg("-f", "--follow[=HOW]", default=FollowMode::Descriptor)] + #[arg("-f", "--follow[=HOW]", value = FollowMode::Descriptor)] Follow(FollowMode), #[arg("-F")] diff --git a/tests/options.rs b/tests/options.rs index 729a107..d52dfe7 100644 --- a/tests/options.rs +++ b/tests/options.rs @@ -357,7 +357,7 @@ fn ls_classify() { enum Arg { #[arg( "-F", "--classify[=WHEN]", - default = When::Always, + value = When::Always, )] Classify(When), } @@ -397,7 +397,7 @@ fn mktemp_tmpdir() { enum Arg { #[arg( "-p DIR", "--tmpdir[=DIR]", - default = String::from("/tmp"), + value = String::from("/tmp"), )] TmpDir(String), } From 217f57b7a3824ee5d1954cbc89d9966475d7a2f1 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Thu, 7 Dec 2023 13:34:57 +0100 Subject: [PATCH 017/116] extract printing flags to a function --- derive/src/help.rs | 31 +------------------------------ src/lib.rs | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 30 deletions(-) diff --git a/derive/src/help.rs b/derive/src/help.rs index 4ab16ba..877a3d8 100644 --- a/derive/src/help.rs +++ b/derive/src/help.rs @@ -75,36 +75,7 @@ pub fn help_string( } let options = if !options.is_empty() { - let options = quote!([#(#options),*]); - quote!( - writeln!(w, "\nOptions:")?; - for (flags, help_string) in #options { - let indent = " ".repeat(#indent); - - let mut help_lines = help_string.lines(); - write!(w, "{}", &indent)?; - write!(w, "{}", &flags)?; - - if flags.len() <= #width { - let line = match help_lines.next() { - Some(line) => line, - None => { - writeln!(w)?; - continue; - }, - }; - let help_indent = " ".repeat(#width-flags.len()+2); - writeln!(w, "{}{}", help_indent, line)?; - } else { - writeln!(w, "\n")?; - } - - let help_indent = " ".repeat(#width+#indent+2); - for line in help_lines { - writeln!(w, "{}{}", help_indent, line)?; - } - } - ) + quote!(::uutils_args::print_flags(&mut w, #indent, #width, [#(#options),*])?;) } else { quote!() }; diff --git a/src/lib.rs b/src/lib.rs index 72321c8..96c1a38 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,7 @@ pub use value::{Value, ValueError, ValueResult}; use std::{ ffi::{OsStr, OsString}, + io::Write, marker::PhantomData, }; @@ -316,6 +317,40 @@ pub fn filter_suggestions(input: &str, long_options: &[&str], prefix: &str) -> V .collect() } +pub fn print_flags( + mut w: impl Write, + indent_size: usize, + width: usize, + options: impl IntoIterator, +) -> std::io::Result<()> { + let indent = " ".repeat(indent_size); + writeln!(w, "\nOptions:")?; + for (flags, help_string) in options { + let mut help_lines = help_string.lines(); + write!(w, "{}{}", &indent, &flags)?; + + if flags.len() <= width { + let line = match help_lines.next() { + Some(line) => line, + None => { + writeln!(w)?; + continue; + } + }; + let help_indent = " ".repeat(width - flags.len() + 2); + writeln!(w, "{}{}", help_indent, line)?; + } else { + writeln!(w)?; + } + + let help_indent = " ".repeat(width + indent_size + 2); + for line in help_lines { + writeln!(w, "{}{}", help_indent, line)?; + } + } + Ok(()) +} + #[cfg(test)] mod test { use std::ffi::OsStr; From 5f84e8b958aa0e4a3ab2612fd1b2ab3a0f9ddd21 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Thu, 7 Dec 2023 15:31:32 +0100 Subject: [PATCH 018/116] initial implementation of completion for fish --- .github/workflows/ci.yml | 2 +- Cargo.toml | 9 ++++-- complete/Cargo.toml | 10 ++++++ complete/LICENSE | 1 + complete/src/fish.rs | 67 ++++++++++++++++++++++++++++++++++++++++ complete/src/lib.rs | 40 ++++++++++++++++++++++++ derive/src/complete.rs | 51 ++++++++++++++++++++++++++++++ derive/src/lib.rs | 9 +++++- examples/hello_world.rs | 8 ++--- src/lib.rs | 49 ++++++++++++++++++++++++++--- src/value.rs | 17 ++++++++++ 11 files changed, 247 insertions(+), 16 deletions(-) create mode 100644 complete/Cargo.toml create mode 120000 complete/LICENSE create mode 100644 complete/src/fish.rs create mode 100644 complete/src/lib.rs create mode 100644 derive/src/complete.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3039939..c03e0c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: - uses: actions-rs/cargo@v1 with: command: test - args: --all-features --workspace + args: --features complete --workspace rustfmt: name: Rustfmt diff --git a/Cargo.toml b/Cargo.toml index 415b33a..138b034 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,10 +11,13 @@ readme = "README.md" [dependencies] uutils-args-derive = { version = "0.1.0", path = "derive" } +uutils-args-complete = { version = "0.1.0", path = "complete", optional = true } strsim = "0.10.0" lexopt = "0.3.0" +[features] +parse-is-complete = ["complete"] +complete = ["uutils-args-complete"] + [workspace] -members = [ - "derive", -] +members = ["derive", "complete"] diff --git a/complete/Cargo.toml b/complete/Cargo.toml new file mode 100644 index 0000000..54abe1a --- /dev/null +++ b/complete/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "uutils-args-complete" +version = "0.1.0" +edition = "2021" +authors = ["Terts Diepraam"] +license = "MIT" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/complete/LICENSE b/complete/LICENSE new file mode 120000 index 0000000..ea5b606 --- /dev/null +++ b/complete/LICENSE @@ -0,0 +1 @@ +../LICENSE \ No newline at end of file diff --git a/complete/src/fish.rs b/complete/src/fish.rs new file mode 100644 index 0000000..8c42c7d --- /dev/null +++ b/complete/src/fish.rs @@ -0,0 +1,67 @@ +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use crate::{Command, ValueHint}; + +pub fn render(c: &Command) -> String { + let mut out = String::new(); + let name = &c.name; + for arg in &c.args { + let mut line = format!("complete -c {name}"); + for short in &arg.short { + line.push_str(&format!(" -s {short}")); + } + for long in &arg.long { + line.push_str(&format!(" -l {long}")); + } + line.push_str(&format!(" -d '{}'", arg.help)); + if let Some(value) = &arg.value { + line.push_str(&render_value_hint(value)); + } + out.push_str(&line); + out.push('\n'); + } + out +} + +fn render_value_hint(value: &ValueHint) -> String { + match value { + ValueHint::Strings(s) => { + let joined = s.join(", "); + format!(" -a {{ {joined} }}") + } + _ => todo!(), + } +} + +#[cfg(test)] +mod test { + use super::render; + use crate::{Arg, Command}; + + #[test] + fn short() { + let c = Command { + name: "test".into(), + args: vec![Arg { + short: vec!["a".into()], + help: "some flag".into(), + ..Arg::default() + }], + }; + assert_eq!(render(&c), "complete -c test -s a -d 'some flag'\n",) + } + + #[test] + fn long() { + let c = Command { + name: "test".into(), + args: vec![Arg { + long: vec!["all".into()], + help: "some flag".into(), + ..Arg::default() + }], + }; + assert_eq!(render(&c), "complete -c test -l all -d 'some flag'\n",) + } +} diff --git a/complete/src/lib.rs b/complete/src/lib.rs new file mode 100644 index 0000000..32a88fb --- /dev/null +++ b/complete/src/lib.rs @@ -0,0 +1,40 @@ +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +mod fish; + +pub struct Command { + pub name: String, + pub args: Vec, +} + +#[derive(Default)] +pub struct Arg { + pub short: Vec, + pub long: Vec, + pub help: String, + pub value: Option, +} + +pub enum ValueHint { + Strings(Vec), + Unknown, + // Other, + AnyPath, + // FilePath, + // DirPath, + // ExecutablePath, + // CommandName, + // CommandString, + // CommandWithArguments, + // Username, + // Hostname, +} + +pub fn render(c: &Command, shell: &str) -> String { + match shell { + "fish" => fish::render(c), + "sh" | "zsh" | "bash" | "csh" => panic!("shell '{shell}' completion is not supported yet!"), + _ => panic!("unknown shell '{shell}'!"), + } +} diff --git a/derive/src/complete.rs b/derive/src/complete.rs new file mode 100644 index 0000000..92bcf98 --- /dev/null +++ b/derive/src/complete.rs @@ -0,0 +1,51 @@ +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use crate::{ + argument::{ArgType, Argument}, + flags::{Flag, Flags}, +}; +use proc_macro2::TokenStream; +use quote::quote; + +pub fn complete(args: &[Argument]) -> TokenStream { + let mut arg_specs = Vec::new(); + + for Argument { help, arg_type, .. } in args { + let ArgType::Option { + flags, + hidden: false, + .. + } = arg_type + else { + continue; + }; + + let Flags { short, long, .. } = flags; + if short.is_empty() && long.is_empty() { + continue; + } + let short: Vec<_> = short + .iter() + .map(|Flag { flag, .. }| quote!(String::from(#flag))) + .collect(); + let long: Vec<_> = long + .iter() + .map(|Flag { flag, .. }| quote!(String::from(#flag))) + .collect(); + + arg_specs.push(quote!( + Arg { + short: vec![#(#short),*], + long: vec![#(#long),*], + help: String::from(#help), + value: None, + } + )) + } + + quote!(Command { + name: String::from(option_env!("CARGO_BIN_NAME").unwrap_or(env!("CARGO_PKG_NAME"))), + args: vec![#(#arg_specs),*] + }) +} diff --git a/derive/src/lib.rs b/derive/src/lib.rs index fc463bf..79406b9 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -3,6 +3,7 @@ mod argument; mod attributes; +mod complete; mod flags; mod help; mod help_parser; @@ -44,6 +45,7 @@ pub fn arguments(input: TokenStream) -> TokenStream { &arguments_attr.version_flags, &arguments_attr.file, ); + let completion = complete::complete(&arguments); let help = help_handling(&arguments_attr.help_flags); let version = version_handling(&arguments_attr.version_flags); let version_string = quote!(format!( @@ -74,7 +76,6 @@ pub fn arguments(input: TokenStream) -> TokenStream { ) -> Result>, uutils_args::Error> { use uutils_args::{Value, lexopt, Error, Argument}; - // #number_argment #free let arg = match { #next_arg } { @@ -104,6 +105,12 @@ pub fn arguments(input: TokenStream) -> TokenStream { fn version() -> String { #version_string } + + #[cfg(feature = "complete")] + fn complete() -> ::uutils_args_complete::Command { + use ::uutils_args_complete::{Command, Arg, ValueHint}; + #completion + } } ); diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 66137b4..1adf4b1 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -3,15 +3,11 @@ use uutils_args::{Arguments, Options}; #[derive(Arguments)] #[arguments(file = "examples/hello_world_help.md")] enum Arg { - /// The *name* to **greet** - /// - /// Just to show off, I can do multiple paragraphs and wrap text! - /// - /// # Also headings! + /// The name to greet #[arg("-n NAME", "--name=NAME", "name=NAME")] Name(String), - /// The **number of times** to `greet` + /// The number of times to greet #[arg("-c N", "--count=N")] Count(u8), diff --git a/src/lib.rs b/src/lib.rs index 604e21f..4b429bf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -108,6 +108,9 @@ pub trait Arguments: Sized { while iter.next_arg()?.is_some() {} Ok(()) } + + #[cfg(feature = "complete")] + fn complete() -> uutils_args_complete::Command; } /// An iterator over arguments. @@ -192,12 +195,32 @@ pub trait Options: Sized { I: IntoIterator + 'static, I::Item: Into, { - let mut iter = Arg::parse(args); - while let Some(arg) = iter.next_arg()? { - self.apply(arg); + // Hacky but it works: if the parse-is-complete flag is active the + // parse function becomes the complete function so that no additional + // functionality is necessary for users to generate completions. It is + // important that we exit the program here, because the program does + // not expect us to print the completion here and therefore will behave + // incorrectly. + #[cfg(feature = "parse-is-complete")] + { + print_completion::<_, Self, Arg>(args.into_iter()); + std::process::exit(0); + } + + #[cfg(not(feature = "parse-is-complete"))] + { + let mut iter = Arg::parse(args); + while let Some(arg) = iter.next_arg()? { + self.apply(arg); + } + Arg::check_missing(iter.positional_idx)?; + Ok(self) } - Arg::check_missing(iter.positional_idx)?; - Ok(self) + } + + #[cfg(feature = "complete")] + fn complete(shell: &str) -> String { + uutils_args_complete::render(&Arg::complete(), shell) } } @@ -224,6 +247,22 @@ pub fn __echo_style_positional(p: &mut lexopt::Parser, short_args: &[char]) -> O } } +#[cfg(feature = "parse-is-complete")] +fn print_completion, Arg: Arguments>(mut args: I) +where + I: Iterator + 'static, + I::Item: Into, +{ + let _exec_name = args.next(); + let shell = args + .next() + .expect("Need a shell argument for completion.") + .into(); + let shell = shell.to_string_lossy(); + assert!(args.next().is_none(), "completion only takes one argument"); + println!("{}", O::complete(&shell)); +} + fn is_echo_style_positional(s: &OsStr, short_args: &[char]) -> bool { let s = match s.to_str() { Some(x) => x, diff --git a/src/value.rs b/src/value.rs index e95b9a4..ae9e3f7 100644 --- a/src/value.rs +++ b/src/value.rs @@ -6,6 +6,8 @@ use std::{ ffi::{OsStr, OsString}, path::PathBuf, }; +#[cfg(feature = "complete")] +use uutils_args_complete::ValueHint; pub type ValueResult = Result>; @@ -51,6 +53,11 @@ impl std::fmt::Display for ValueError { /// If an error is returned, it will be wrapped in [`Error::ParsingFailed`] pub trait Value: Sized { fn from_value(value: &OsStr) -> ValueResult; + + #[cfg(feature = "complete")] + fn value_hint() -> ValueHint { + ValueHint::Unknown + } } impl Value for OsString { @@ -63,6 +70,11 @@ impl Value for PathBuf { fn from_value(value: &OsStr) -> ValueResult { Ok(PathBuf::from(value)) } + + #[cfg(feature = "complete")] + fn value_hint() -> ValueHint { + ValueHint::AnyPath + } } impl Value for String { @@ -81,6 +93,11 @@ where fn from_value(value: &OsStr) -> ValueResult { Ok(Some(T::from_value(value)?)) } + + #[cfg(feature = "complete")] + fn value_hint() -> uutils_args_complete::ValueHint { + T::value_hint() + } } macro_rules! value_int { From efe8e323bee326b8bd7e297d955e90538ef7c461 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Thu, 7 Dec 2023 16:30:16 +0100 Subject: [PATCH 019/116] value hints in completions --- complete/src/fish.rs | 1 + derive/src/argument.rs | 2 ++ derive/src/complete.rs | 18 ++++++++++++++++-- derive/src/flags.rs | 2 +- derive/src/lib.rs | 13 +++++++++++++ examples/value.rs | 38 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 examples/value.rs diff --git a/complete/src/fish.rs b/complete/src/fish.rs index 8c42c7d..5bbb451 100644 --- a/complete/src/fish.rs +++ b/complete/src/fish.rs @@ -30,6 +30,7 @@ fn render_value_hint(value: &ValueHint) -> String { let joined = s.join(", "); format!(" -a {{ {joined} }}") } + ValueHint::Unknown => String::new(), _ => todo!(), } } diff --git a/derive/src/argument.rs b/derive/src/argument.rs index 7e3928a..9f0a1c6 100644 --- a/derive/src/argument.rs +++ b/derive/src/argument.rs @@ -14,6 +14,7 @@ use crate::{ pub struct Argument { pub ident: Ident, + pub field: Option, pub name: String, pub arg_type: ArgType, pub help: String, @@ -105,6 +106,7 @@ pub fn parse_argument(v: Variant) -> Vec { }; Argument { ident: ident.clone(), + field: field.clone(), name: name.clone(), arg_type, help: arg_help, diff --git a/derive/src/complete.rs b/derive/src/complete.rs index 92bcf98..b2d4bae 100644 --- a/derive/src/complete.rs +++ b/derive/src/complete.rs @@ -11,7 +11,13 @@ use quote::quote; pub fn complete(args: &[Argument]) -> TokenStream { let mut arg_specs = Vec::new(); - for Argument { help, arg_type, .. } in args { + for Argument { + help, + field, + arg_type, + .. + } in args + { let ArgType::Option { flags, hidden: false, @@ -25,21 +31,29 @@ pub fn complete(args: &[Argument]) -> TokenStream { if short.is_empty() && long.is_empty() { continue; } + let short: Vec<_> = short .iter() .map(|Flag { flag, .. }| quote!(String::from(#flag))) .collect(); + let long: Vec<_> = long .iter() .map(|Flag { flag, .. }| quote!(String::from(#flag))) .collect(); + let hint = if let Some(ty) = field { + quote!(Some(<#ty>::value_hint())) + } else { + quote!(None) + }; + arg_specs.push(quote!( Arg { short: vec![#(#short),*], long: vec![#(#long),*], help: String::from(#help), - value: None, + value: #hint, } )) } diff --git a/derive/src/flags.rs b/derive/src/flags.rs index 82cf489..3d69dca 100644 --- a/derive/src/flags.rs +++ b/derive/src/flags.rs @@ -11,7 +11,7 @@ pub struct Flags { pub dd_style: Vec<(String, String)>, } -#[derive(Clone)] +#[derive(Clone, PartialEq, Eq)] pub enum Value { No, Optional(String), diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 79406b9..23b90da 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -109,6 +109,7 @@ pub fn arguments(input: TokenStream) -> TokenStream { #[cfg(feature = "complete")] fn complete() -> ::uutils_args_complete::Command { use ::uutils_args_complete::{Command, Arg, ValueHint}; + use ::uutils_args::Value; #completion } } @@ -131,6 +132,7 @@ pub fn value(input: TokenStream) -> TokenStream { let mut options = Vec::new(); let mut match_arms = vec![]; + let mut all_keys = Vec::new(); for variant in data.variants { let variant_name = variant.ident.to_string(); let attrs = variant.attrs.clone(); @@ -147,6 +149,7 @@ pub fn value(input: TokenStream) -> TokenStream { keys }; + all_keys.extend(keys.clone()); options.push(quote!(&[#(#keys),*])); let stmt = if let Some(v) = value { @@ -195,6 +198,16 @@ pub fn value(input: TokenStream) -> TokenStream { _ => unreachable!("Should be caught by (None, []) case above.") }) } + + #[cfg(feature = "complete")] + fn value_hint() -> ::uutils_args_complete::ValueHint { + ::uutils_args_complete::ValueHint::Strings( + [#(#all_keys),*] + .into_iter() + .map(ToString::to_string) + .collect() + ) + } } ); diff --git a/examples/value.rs b/examples/value.rs new file mode 100644 index 0000000..a0139f3 --- /dev/null +++ b/examples/value.rs @@ -0,0 +1,38 @@ +use uutils_args::{Arguments, Options, Value}; + +#[derive(Arguments)] +#[arguments(file = "examples/hello_world_help.md")] +enum Arg { + /// Color! + #[arg("-c NAME", "--color=NAME")] + Color(Color), +} + +#[derive(Value, Debug, Default)] +enum Color { + #[value("never")] + Never, + #[default] + #[value("auto")] + Auto, + #[value("always")] + Always, +} + +#[derive(Default)] +struct Settings { + color: Color, +} + +impl Options for Settings { + fn apply(&mut self, arg: Arg) { + match arg { + Arg::Color(c) => self.color = c, + } + } +} + +fn main() { + let color = Settings::default().parse(std::env::args_os()).color; + println!("{:?}", color); +} From 2f4656622114f16e33c2a45559522daf34dd2af7 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Fri, 8 Dec 2023 10:51:50 +0100 Subject: [PATCH 020/116] allow unused mut in try_parse --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index 4b429bf..5aa06bf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -190,6 +190,7 @@ pub trait Options: Sized { exit_if_err(self.try_parse(args), Arg::EXIT_CODE) } + #[allow(unused_mut)] fn try_parse(mut self, args: I) -> Result where I: IntoIterator + 'static, From 2f6c32ddfc520c3f734cc001052a638302f4bdc1 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Fri, 8 Dec 2023 10:59:00 +0100 Subject: [PATCH 021/116] rename completion to complete for consistency --- derive/src/lib.rs | 4 ++-- src/lib.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 23b90da..5df241f 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -45,7 +45,7 @@ pub fn arguments(input: TokenStream) -> TokenStream { &arguments_attr.version_flags, &arguments_attr.file, ); - let completion = complete::complete(&arguments); + let complete_command = complete::complete(&arguments); let help = help_handling(&arguments_attr.help_flags); let version = version_handling(&arguments_attr.version_flags); let version_string = quote!(format!( @@ -110,7 +110,7 @@ pub fn arguments(input: TokenStream) -> TokenStream { fn complete() -> ::uutils_args_complete::Command { use ::uutils_args_complete::{Command, Arg, ValueHint}; use ::uutils_args::Value; - #completion + #complete_command } } ); diff --git a/src/lib.rs b/src/lib.rs index 5aa06bf..22d124b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -204,7 +204,7 @@ pub trait Options: Sized { // incorrectly. #[cfg(feature = "parse-is-complete")] { - print_completion::<_, Self, Arg>(args.into_iter()); + print_complete::<_, Self, Arg>(args.into_iter()); std::process::exit(0); } @@ -249,7 +249,7 @@ pub fn __echo_style_positional(p: &mut lexopt::Parser, short_args: &[char]) -> O } #[cfg(feature = "parse-is-complete")] -fn print_completion, Arg: Arguments>(mut args: I) +fn print_complete, Arg: Arguments>(mut args: I) where I: Iterator + 'static, I::Item: Into, From ba7a40a5f08718f5fdb7abe1cc65def845fee49e Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Fri, 8 Dec 2023 11:26:38 +0100 Subject: [PATCH 022/116] more value hints in fish --- complete/src/fish.rs | 48 +++++++++++++++++++++++++++++++++++++++----- complete/src/lib.rs | 15 ++++++-------- 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/complete/src/fish.rs b/complete/src/fish.rs index 5bbb451..3571b01 100644 --- a/complete/src/fish.rs +++ b/complete/src/fish.rs @@ -27,18 +27,21 @@ pub fn render(c: &Command) -> String { fn render_value_hint(value: &ValueHint) -> String { match value { ValueHint::Strings(s) => { - let joined = s.join(", "); - format!(" -a {{ {joined} }}") + let joined = s.join(" "); + format!(" -f -a \"{joined}\"") } - ValueHint::Unknown => String::new(), - _ => todo!(), + ValueHint::AnyPath | ValueHint::FilePath | ValueHint::ExecutablePath => String::from(" -F"), + ValueHint::DirPath => " -f -a \"(__fish_complete_directories)\"".into(), + ValueHint::Unknown => " -f".into(), + ValueHint::Username => " -f -a \"(__fish_complete_users)\"".into(), + ValueHint::Hostname => " -f -a \"(__fish_print_hostnames)\"".into(), } } #[cfg(test)] mod test { use super::render; - use crate::{Arg, Command}; + use crate::{Arg, Command, ValueHint}; #[test] fn short() { @@ -65,4 +68,39 @@ mod test { }; assert_eq!(render(&c), "complete -c test -l all -d 'some flag'\n",) } + + #[test] + fn value_hints() { + let args = [ + ( + ValueHint::Strings(vec!["all".into(), "none".into()]), + "-f -a \"all none\"", + ), + (ValueHint::Unknown, "-f"), + (ValueHint::AnyPath, "-F"), + (ValueHint::FilePath, "-F"), + ( + ValueHint::DirPath, + "-f -a \"(__fish_complete_directories)\"", + ), + (ValueHint::ExecutablePath, "-F"), + (ValueHint::Username, "-f -a \"(__fish_complete_users)\""), + (ValueHint::Hostname, "-f -a \"(__fish_print_hostnames)\""), + ]; + for (hint, expected) in args { + let c = Command { + name: "test".into(), + args: vec![Arg { + short: vec!["a".into()], + long: vec![], + help: "some flag".into(), + value: Some(hint), + }], + }; + assert_eq!( + render(&c), + format!("complete -c test -s a -d 'some flag' {expected}\n") + ) + } + } } diff --git a/complete/src/lib.rs b/complete/src/lib.rs index 32a88fb..2be302e 100644 --- a/complete/src/lib.rs +++ b/complete/src/lib.rs @@ -16,19 +16,16 @@ pub struct Arg { pub value: Option, } +// Modelled after claps ValueHint pub enum ValueHint { Strings(Vec), Unknown, - // Other, AnyPath, - // FilePath, - // DirPath, - // ExecutablePath, - // CommandName, - // CommandString, - // CommandWithArguments, - // Username, - // Hostname, + FilePath, + DirPath, + ExecutablePath, + Username, + Hostname, } pub fn render(c: &Command, shell: &str) -> String { From 675bbb95c85e4d0c05731f726de6ed0738ebe0bc Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Fri, 8 Dec 2023 11:45:08 +0100 Subject: [PATCH 023/116] add elvish and powershell to unimplemented shells --- complete/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/complete/src/lib.rs b/complete/src/lib.rs index 2be302e..949b224 100644 --- a/complete/src/lib.rs +++ b/complete/src/lib.rs @@ -31,7 +31,7 @@ pub enum ValueHint { pub fn render(c: &Command, shell: &str) -> String { match shell { "fish" => fish::render(c), - "sh" | "zsh" | "bash" | "csh" => panic!("shell '{shell}' completion is not supported yet!"), + "sh" | "zsh" | "bash" | "csh" | "elvish" | "powershell" => panic!("shell '{shell}' completion is not supported yet!"), _ => panic!("unknown shell '{shell}'!"), } } From c30d5e893bdb30b2cfee1b1868f853f4b7d64e91 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Fri, 8 Dec 2023 13:20:03 +0100 Subject: [PATCH 024/116] super basic zsh completion --- complete/src/lib.rs | 4 +++- complete/src/zsh.rs | 58 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 complete/src/zsh.rs diff --git a/complete/src/lib.rs b/complete/src/lib.rs index 949b224..a6e5365 100644 --- a/complete/src/lib.rs +++ b/complete/src/lib.rs @@ -2,6 +2,7 @@ // file that was distributed with this source code. mod fish; +mod zsh; pub struct Command { pub name: String, @@ -31,7 +32,8 @@ pub enum ValueHint { pub fn render(c: &Command, shell: &str) -> String { match shell { "fish" => fish::render(c), - "sh" | "zsh" | "bash" | "csh" | "elvish" | "powershell" => panic!("shell '{shell}' completion is not supported yet!"), + "zsh" => zsh::render(c), + "sh" | "bash" | "csh" | "elvish" | "powershell" => panic!("shell '{shell}' completion is not supported yet!"), _ => panic!("unknown shell '{shell}'!"), } } diff --git a/complete/src/zsh.rs b/complete/src/zsh.rs new file mode 100644 index 0000000..143596f --- /dev/null +++ b/complete/src/zsh.rs @@ -0,0 +1,58 @@ +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use crate::{Command, Arg}; + +pub fn render(c: &Command) -> String { + template(&c.name, &render_args(&c.args)) +} + +fn render_args(args: &[Arg]) -> String { + let mut out = String::new(); + let indent = " ".repeat(8); + for arg in args { + let help = &arg.help; + for short in &arg.short { + out.push_str( + &format!("{indent}'-{short}[{help}]' \\\n") + ); + } + for long in &arg.long { + out.push_str( + &format!("{indent}'--{long}[{help}]' \\\n") + ); + } + } + out +} + +fn template(name: &str, args: &str) -> String { + format!( + "\ +#compdef {name} + +autoload -U is-at-least + +_{name}() {{ + typeset -A opt_args + typeset -a _arguments_options + local ret=1 + + if is-at-least 5.2; then + _arguments_options=(-s -S -C) + else + _arguments_options=(-s -C) + fi + + local context curcontext=\"$curcontext\" state line + _arguments \"${{_arguments_options[@]}}\" \\\n{args} +&& ret=0 +}} + +if [ \"$funcstack[1]\" = \"_{name}\" ]; then + {name} \"$@\" +else + compdef _{name} {name} +fi" + ) +} \ No newline at end of file From 5600b4a4a699f9cdb78decd1c21a2ce22c904205 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Fri, 8 Dec 2023 14:09:57 +0100 Subject: [PATCH 025/116] add basic mdbook rendering --- complete/src/fish.rs | 4 +++ complete/src/lib.rs | 11 ++++++-- complete/src/md.rs | 57 ++++++++++++++++++++++++++++++++++++++++++ complete/src/zsh.rs | 13 ++++------ derive/src/complete.rs | 11 +++++++- derive/src/help.rs | 2 +- derive/src/lib.rs | 2 +- 7 files changed, 87 insertions(+), 13 deletions(-) create mode 100644 complete/src/md.rs diff --git a/complete/src/fish.rs b/complete/src/fish.rs index 3571b01..f86fc93 100644 --- a/complete/src/fish.rs +++ b/complete/src/fish.rs @@ -3,6 +3,7 @@ use crate::{Command, ValueHint}; +/// Create completion script for `fish` pub fn render(c: &Command) -> String { let mut out = String::new(); let name = &c.name; @@ -52,6 +53,7 @@ mod test { help: "some flag".into(), ..Arg::default() }], + ..Command::default() }; assert_eq!(render(&c), "complete -c test -s a -d 'some flag'\n",) } @@ -65,6 +67,7 @@ mod test { help: "some flag".into(), ..Arg::default() }], + ..Command::default() }; assert_eq!(render(&c), "complete -c test -l all -d 'some flag'\n",) } @@ -96,6 +99,7 @@ mod test { help: "some flag".into(), value: Some(hint), }], + ..Command::default() }; assert_eq!( render(&c), diff --git a/complete/src/lib.rs b/complete/src/lib.rs index a6e5365..4504797 100644 --- a/complete/src/lib.rs +++ b/complete/src/lib.rs @@ -2,10 +2,15 @@ // file that was distributed with this source code. mod fish; +mod md; mod zsh; +#[derive(Default)] pub struct Command { pub name: String, + pub summary: String, + pub version: String, + pub after_options: String, pub args: Vec, } @@ -31,9 +36,11 @@ pub enum ValueHint { pub fn render(c: &Command, shell: &str) -> String { match shell { + "md" => md::render(c), "fish" => fish::render(c), "zsh" => zsh::render(c), - "sh" | "bash" | "csh" | "elvish" | "powershell" => panic!("shell '{shell}' completion is not supported yet!"), - _ => panic!("unknown shell '{shell}'!"), + "man" => panic!("manpages are not implemented yet"), + "sh" | "bash" | "csh" | "elvish" | "powershell" => panic!("shell '{shell}' completion is not implemented yet!"), + _ => panic!("unknown option '{shell}'! Expected one of: \"md\", \"fish\", \"zsh\", \"man\", \"sh\", \"bash\", \"csh\", \"elvish\", \"powershell\""), } } diff --git a/complete/src/md.rs b/complete/src/md.rs new file mode 100644 index 0000000..08f502f --- /dev/null +++ b/complete/src/md.rs @@ -0,0 +1,57 @@ +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use crate::Command; + +/// Render command to a markdown file for mdbook +pub fn render(c: &Command) -> String { + let mut out = String::new(); + out.push_str(&title(c)); + out.push_str(&additional(c)); + out.push_str(&c.summary); + out.push_str("\n\n"); + out.push_str(&options(c)); + out.push_str("\n\n"); + out.push_str(&c.after_options); + out.push('\n'); + out +} + +fn title(c: &Command) -> String { + format!("# {}\n\n", c.name) +} + +fn additional(c: &Command) -> String { + let version = &c.version; + format!( + "\ +
\ + {version}\ +
\n\n\ + " + ) +} + +fn options(c: &Command) -> String { + let mut out = String::from("## Options\n\n"); + out.push_str("
\n"); + for arg in &c.args { + out.push_str("
"); + + let mut flags = Vec::new(); + + for long in &arg.long { + flags.push(format!("--{long}")); + } + + for short in &arg.short { + flags.push(format!("-{short}")) + } + + out.push_str(&flags.join(", ")); + out.push_str("
\n"); + out.push_str(&format!("
\n\n{}\n\n
\n", arg.help)); + } + out.push_str("
"); + out +} diff --git a/complete/src/zsh.rs b/complete/src/zsh.rs index 143596f..814c904 100644 --- a/complete/src/zsh.rs +++ b/complete/src/zsh.rs @@ -1,8 +1,9 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::{Command, Arg}; +use crate::{Arg, Command}; +/// Create completion script for `zsh` pub fn render(c: &Command) -> String { template(&c.name, &render_args(&c.args)) } @@ -13,14 +14,10 @@ fn render_args(args: &[Arg]) -> String { for arg in args { let help = &arg.help; for short in &arg.short { - out.push_str( - &format!("{indent}'-{short}[{help}]' \\\n") - ); + out.push_str(&format!("{indent}'-{short}[{help}]' \\\n")); } for long in &arg.long { - out.push_str( - &format!("{indent}'--{long}[{help}]' \\\n") - ); + out.push_str(&format!("{indent}'--{long}[{help}]' \\\n")); } } out @@ -55,4 +52,4 @@ else compdef _{name} {name} fi" ) -} \ No newline at end of file +} diff --git a/derive/src/complete.rs b/derive/src/complete.rs index b2d4bae..2d7b365 100644 --- a/derive/src/complete.rs +++ b/derive/src/complete.rs @@ -8,9 +8,15 @@ use crate::{ use proc_macro2::TokenStream; use quote::quote; -pub fn complete(args: &[Argument]) -> TokenStream { +pub fn complete(args: &[Argument], file: &Option) -> TokenStream { let mut arg_specs = Vec::new(); + let (summary, _usage, after_options) = if let Some(file) = file { + crate::help::read_help_file(file) + } else { + ("".into(), "{} [OPTIONS] [ARGUMENTS]".into(), "".into()) + }; + for Argument { help, field, @@ -60,6 +66,9 @@ pub fn complete(args: &[Argument]) -> TokenStream { quote!(Command { name: String::from(option_env!("CARGO_BIN_NAME").unwrap_or(env!("CARGO_PKG_NAME"))), + summary: String::from(#summary), + after_options: String::from(#after_options), + version: String::from(env!("CARGO_PKG_VERSION")), args: vec![#(#arg_specs),*] }) } diff --git a/derive/src/help.rs b/derive/src/help.rs index 877a3d8..9ebeaf2 100644 --- a/derive/src/help.rs +++ b/derive/src/help.rs @@ -99,7 +99,7 @@ pub fn help_string( ) } -fn read_help_file(file: &str) -> (String, String, 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 mut location = PathBuf::from(manifest_dir); diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 5df241f..e855127 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -45,7 +45,7 @@ pub fn arguments(input: TokenStream) -> TokenStream { &arguments_attr.version_flags, &arguments_attr.file, ); - let complete_command = complete::complete(&arguments); + let complete_command = complete::complete(&arguments, &arguments_attr.file); let help = help_handling(&arguments_attr.help_flags); let version = version_handling(&arguments_attr.version_flags); let version_string = quote!(format!( From 72894a920e2c2d03ea8534ebfce921aaf2ecabf6 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Fri, 8 Dec 2023 16:14:15 +0100 Subject: [PATCH 026/116] initial man page generation --- .vscode/settings.json | 1 + complete/Cargo.toml | 1 + complete/src/lib.rs | 3 ++- complete/src/man.rs | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 complete/src/man.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index ae1e84e..7fca93f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,6 +11,7 @@ "Nonblank", "nonprinting", "pico", + "Roff", "struct", "uutils", "xflags" diff --git a/complete/Cargo.toml b/complete/Cargo.toml index 54abe1a..16c73d0 100644 --- a/complete/Cargo.toml +++ b/complete/Cargo.toml @@ -8,3 +8,4 @@ license = "MIT" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +roff = "0.2.1" \ No newline at end of file diff --git a/complete/src/lib.rs b/complete/src/lib.rs index 4504797..b703d64 100644 --- a/complete/src/lib.rs +++ b/complete/src/lib.rs @@ -2,6 +2,7 @@ // file that was distributed with this source code. mod fish; +mod man; mod md; mod zsh; @@ -39,7 +40,7 @@ pub fn render(c: &Command, shell: &str) -> String { "md" => md::render(c), "fish" => fish::render(c), "zsh" => zsh::render(c), - "man" => panic!("manpages are not implemented yet"), + "man" => man::render(c), "sh" | "bash" | "csh" | "elvish" | "powershell" => panic!("shell '{shell}' completion is not implemented yet!"), _ => panic!("unknown option '{shell}'! Expected one of: \"md\", \"fish\", \"zsh\", \"man\", \"sh\", \"bash\", \"csh\", \"elvish\", \"powershell\""), } diff --git a/complete/src/man.rs b/complete/src/man.rs new file mode 100644 index 0000000..44500a6 --- /dev/null +++ b/complete/src/man.rs @@ -0,0 +1,33 @@ +use crate::Command; +use roff::{bold, roman, Roff}; + +pub fn render(c: &Command) -> String { + let mut page = Roff::new(); + page.control("TH", [&c.name.to_uppercase(), "1"]); + page.control("SH", ["NAME"]); + page.text([roman(&c.name)]); + page.control("SH", ["DESCRIPTION"]); + page.text([roman(&c.summary)]); + page.control("SH", ["OPTIONS"]); + + for arg in &c.args { + page.control("TP", []); + + let mut flags = Vec::new(); + for l in &arg.long { + if !flags.is_empty() { + flags.push(roman(", ")); + } + flags.push(bold(format!("--{l}"))); + } + for s in &arg.short { + if !flags.is_empty() { + flags.push(roman(", ")); + } + flags.push(bold(format!("-{s}"))); + } + page.text(flags); + page.text([roman(&arg.help)]); + } + page.render() +} From c4cb26cab8e5d54952a6861fbc47820067d5f3e1 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Sat, 9 Dec 2023 14:37:02 +0100 Subject: [PATCH 027/116] add license header to man.rs --- complete/src/man.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/complete/src/man.rs b/complete/src/man.rs index 44500a6..34d92aa 100644 --- a/complete/src/man.rs +++ b/complete/src/man.rs @@ -1,3 +1,6 @@ +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + use crate::Command; use roff::{bold, roman, Roff}; From cab0fb2cc9220b0f2e6b620adeb18130d01a1cc4 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Sat, 9 Dec 2023 16:51:45 +0100 Subject: [PATCH 028/116] add value names for flags in md and man --- complete/src/fish.rs | 42 ++++++++++++++++++++++------------- complete/src/lib.rs | 50 +++++++++++++++++++++++++++++++++--------- complete/src/man.rs | 43 ++++++++++++++++++++++++++++-------- complete/src/md.rs | 24 ++++++++++++++------ complete/src/zsh.rs | 12 +++++----- derive/src/complete.rs | 41 +++++++++++++++++++++++++--------- derive/src/lib.rs | 3 +-- src/lib.rs | 2 +- 8 files changed, 157 insertions(+), 60 deletions(-) diff --git a/complete/src/fish.rs b/complete/src/fish.rs index f86fc93..d6f0f66 100644 --- a/complete/src/fish.rs +++ b/complete/src/fish.rs @@ -1,19 +1,22 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::{Command, ValueHint}; +use crate::{Command, Flag, ValueHint}; /// Create completion script for `fish` +/// +/// Short and long options are combined into single `complete` calls, even if +/// they differ in whether they take arguments or not. pub fn render(c: &Command) -> String { let mut out = String::new(); let name = &c.name; for arg in &c.args { let mut line = format!("complete -c {name}"); - for short in &arg.short { - line.push_str(&format!(" -s {short}")); + for Flag { flag, .. } in &arg.short { + line.push_str(&format!(" -s {flag}")); } - for long in &arg.long { - line.push_str(&format!(" -l {long}")); + for Flag { flag, .. } in &arg.long { + line.push_str(&format!(" -l {flag}")); } line.push_str(&format!(" -d '{}'", arg.help)); if let Some(value) = &arg.value { @@ -42,15 +45,18 @@ fn render_value_hint(value: &ValueHint) -> String { #[cfg(test)] mod test { use super::render; - use crate::{Arg, Command, ValueHint}; + use crate::{Arg, Command, Flag, Value, ValueHint}; #[test] fn short() { let c = Command { - name: "test".into(), + name: "test", args: vec![Arg { - short: vec!["a".into()], - help: "some flag".into(), + short: vec![Flag { + flag: "a", + value: Value::No, + }], + help: "some flag", ..Arg::default() }], ..Command::default() @@ -61,10 +67,13 @@ mod test { #[test] fn long() { let c = Command { - name: "test".into(), + name: "test", args: vec![Arg { - long: vec!["all".into()], - help: "some flag".into(), + long: vec![Flag { + flag: "all", + value: Value::No, + }], + help: "some flag", ..Arg::default() }], ..Command::default() @@ -92,11 +101,14 @@ mod test { ]; for (hint, expected) in args { let c = Command { - name: "test".into(), + name: "test", args: vec![Arg { - short: vec!["a".into()], + short: vec![Flag { + flag: "a", + value: Value::No, + }], long: vec![], - help: "some flag".into(), + help: "some flag", value: Some(hint), }], ..Command::default() diff --git a/complete/src/lib.rs b/complete/src/lib.rs index b703d64..14a3a47 100644 --- a/complete/src/lib.rs +++ b/complete/src/lib.rs @@ -1,28 +1,58 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +//! Generation of completion and documentation +//! +//! All formats use the [`Command`] struct as input, which specifies all +//! information needed. This struct is similar to some structs in the derive +//! crate for uutils-args, but there are some key differences: +//! +//! - This is meant to be more general. +//! - Some information is added (such as fields for the summary) +//! - We have [`ValueHint`] in this crate. +//! - Some information is removed because it is irrelevant for completion and documentation +//! - This struct is meant to exist at runtime of the program +//! mod fish; mod man; mod md; mod zsh; +/// A description of a CLI command +/// +/// The completions and documentation will be generated based on this struct. #[derive(Default)] -pub struct Command { - pub name: String, - pub summary: String, - pub version: String, - pub after_options: String, - pub args: Vec, +pub struct Command<'a> { + pub name: &'a str, + pub summary: &'a str, + pub version: &'a str, + pub after_options: &'a str, + pub args: Vec>, } +/// Description of an argument +/// +/// An argument may consist of several flags. In completions and documentation +/// formats that support it, these flags will be grouped. #[derive(Default)] -pub struct Arg { - pub short: Vec, - pub long: Vec, - pub help: String, +pub struct Arg<'a> { + pub short: Vec>, + pub long: Vec>, + pub help: &'a str, pub value: Option, } +pub struct Flag<'a> { + pub flag: &'a str, + pub value: Value<'a>, +} + +pub enum Value<'a> { + Required(&'a str), + Optional(&'a str), + No, +} + // Modelled after claps ValueHint pub enum ValueHint { Strings(Vec), diff --git a/complete/src/man.rs b/complete/src/man.rs index 34d92aa..494f828 100644 --- a/complete/src/man.rs +++ b/complete/src/man.rs @@ -1,36 +1,61 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::Command; -use roff::{bold, roman, Roff}; +use crate::{Command, Flag, Value}; +use roff::{bold, italic, roman, Roff}; pub fn render(c: &Command) -> String { let mut page = Roff::new(); page.control("TH", [&c.name.to_uppercase(), "1"]); page.control("SH", ["NAME"]); - page.text([roman(&c.name)]); + page.text([roman(c.name)]); page.control("SH", ["DESCRIPTION"]); - page.text([roman(&c.summary)]); + page.text([roman(c.summary)]); page.control("SH", ["OPTIONS"]); for arg in &c.args { page.control("TP", []); let mut flags = Vec::new(); - for l in &arg.long { + for Flag { flag, value } in &arg.long { if !flags.is_empty() { flags.push(roman(", ")); } - flags.push(bold(format!("--{l}"))); + flags.push(bold(format!("--{flag}"))); + match value { + Value::Required(name) => { + flags.push(roman("=")); + flags.push(italic(*name)); + } + Value::Optional(name) => { + flags.push(roman("[")); + flags.push(roman("=")); + flags.push(italic(*name)); + flags.push(roman("]")); + } + Value::No => {} + } } - for s in &arg.short { + for Flag { flag, value } in &arg.short { if !flags.is_empty() { flags.push(roman(", ")); } - flags.push(bold(format!("-{s}"))); + flags.push(bold(format!("-{flag}"))); + match value { + Value::Required(name) => { + flags.push(roman(" ")); + flags.push(italic(*name)); + } + Value::Optional(name) => { + flags.push(roman("[")); + flags.push(italic(*name)); + flags.push(roman("]")); + } + Value::No => {} + } } page.text(flags); - page.text([roman(&arg.help)]); + page.text([roman(arg.help)]); } page.render() } diff --git a/complete/src/md.rs b/complete/src/md.rs index 08f502f..1c2fa1b 100644 --- a/complete/src/md.rs +++ b/complete/src/md.rs @@ -1,18 +1,18 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::Command; +use crate::{Command, Flag, Value}; /// Render command to a markdown file for mdbook pub fn render(c: &Command) -> String { let mut out = String::new(); out.push_str(&title(c)); out.push_str(&additional(c)); - out.push_str(&c.summary); + out.push_str(c.summary); out.push_str("\n\n"); out.push_str(&options(c)); out.push_str("\n\n"); - out.push_str(&c.after_options); + out.push_str(c.after_options); out.push('\n'); out } @@ -40,12 +40,22 @@ fn options(c: &Command) -> String { let mut flags = Vec::new(); - for long in &arg.long { - flags.push(format!("--{long}")); + for Flag { flag, value } in &arg.long { + let value_str = match value { + Value::Required(name) => format!("={name}"), + Value::Optional(name) => format!("[={name}]"), + Value::No => String::new(), + }; + flags.push(format!("--{flag}{value_str}")); } - for short in &arg.short { - flags.push(format!("-{short}")) + for Flag { flag, value } in &arg.short { + let value_str = match value { + Value::Required(name) => format!(" {name}"), + Value::Optional(name) => format!("[{name}]"), + Value::No => String::new(), + }; + flags.push(format!("-{flag}{value_str}")); } out.push_str(&flags.join(", ")); diff --git a/complete/src/zsh.rs b/complete/src/zsh.rs index 814c904..5087697 100644 --- a/complete/src/zsh.rs +++ b/complete/src/zsh.rs @@ -1,11 +1,11 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::{Arg, Command}; +use crate::{Arg, Command, Flag}; /// Create completion script for `zsh` pub fn render(c: &Command) -> String { - template(&c.name, &render_args(&c.args)) + template(c.name, &render_args(&c.args)) } fn render_args(args: &[Arg]) -> String { @@ -13,11 +13,11 @@ fn render_args(args: &[Arg]) -> String { let indent = " ".repeat(8); for arg in args { let help = &arg.help; - for short in &arg.short { - out.push_str(&format!("{indent}'-{short}[{help}]' \\\n")); + for Flag { flag, .. } in &arg.short { + out.push_str(&format!("{indent}'-{flag}[{help}]' \\\n")); } - for long in &arg.long { - out.push_str(&format!("{indent}'--{long}[{help}]' \\\n")); + for Flag { flag, .. } in &arg.long { + out.push_str(&format!("{indent}'--{flag}[{help}]' \\\n")); } } out diff --git a/derive/src/complete.rs b/derive/src/complete.rs index 2d7b365..9e9ea96 100644 --- a/derive/src/complete.rs +++ b/derive/src/complete.rs @@ -3,7 +3,7 @@ use crate::{ argument::{ArgType, Argument}, - flags::{Flag, Flags}, + flags::{Flag, Flags, Value}, }; use proc_macro2::TokenStream; use quote::quote; @@ -40,12 +40,33 @@ pub fn complete(args: &[Argument], file: &Option) -> TokenStream { let short: Vec<_> = short .iter() - .map(|Flag { flag, .. }| quote!(String::from(#flag))) + .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::Optional(#name)), + }; + quote!(::uutils_args_complete::Flag { + flag: #flag, + value: #value + }) + }) .collect(); let long: Vec<_> = long .iter() - .map(|Flag { flag, .. }| quote!(String::from(#flag))) + .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::Optional(#name)), + }; + quote!(::uutils_args_complete::Flag { + flag: #flag, + value: #value + }) + }) .collect(); let hint = if let Some(ty) = field { @@ -55,20 +76,20 @@ pub fn complete(args: &[Argument], file: &Option) -> TokenStream { }; arg_specs.push(quote!( - Arg { + ::uutils_args_complete::Arg { short: vec![#(#short),*], long: vec![#(#long),*], - help: String::from(#help), + help: #help, value: #hint, } )) } - quote!(Command { - name: String::from(option_env!("CARGO_BIN_NAME").unwrap_or(env!("CARGO_PKG_NAME"))), - summary: String::from(#summary), - after_options: String::from(#after_options), - version: String::from(env!("CARGO_PKG_VERSION")), + quote!(::uutils_args_complete::Command { + name: option_env!("CARGO_BIN_NAME").unwrap_or(env!("CARGO_PKG_NAME")), + summary: #summary, + after_options: #after_options, + version: env!("CARGO_PKG_VERSION"), args: vec![#(#arg_specs),*] }) } diff --git a/derive/src/lib.rs b/derive/src/lib.rs index e855127..cd8ccbf 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -107,8 +107,7 @@ pub fn arguments(input: TokenStream) -> TokenStream { } #[cfg(feature = "complete")] - fn complete() -> ::uutils_args_complete::Command { - use ::uutils_args_complete::{Command, Arg, ValueHint}; + fn complete() -> ::uutils_args_complete::Command<'static> { use ::uutils_args::Value; #complete_command } diff --git a/src/lib.rs b/src/lib.rs index 22d124b..5ec42d2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -110,7 +110,7 @@ pub trait Arguments: Sized { } #[cfg(feature = "complete")] - fn complete() -> uutils_args_complete::Command; + fn complete() -> uutils_args_complete::Command<'static>; } /// An iterator over arguments. From 64d7511585786d0864a9d2de8cc9b862a6eb3388 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 04:48:33 +0000 Subject: [PATCH 029/116] fix(deps): update rust crate syn to 2.0.40 --- derive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 0901f50..4d86544 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -11,4 +11,4 @@ proc_macro = true [dependencies] proc-macro2 = "1.0.70" quote = "1.0.33" -syn = { version = "2.0.39", features = ["full"] } +syn = { version = "2.0.40", features = ["full"] } From 0bf84d564251b3f3f6060f1af9f7caf0a419112e Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Sat, 9 Dec 2023 14:28:11 +0100 Subject: [PATCH 030/116] add authors and copyright info to man page --- complete/src/lib.rs | 2 ++ complete/src/man.rs | 7 +++++++ derive/src/complete.rs | 4 +++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/complete/src/lib.rs b/complete/src/lib.rs index 14a3a47..15dc7ba 100644 --- a/complete/src/lib.rs +++ b/complete/src/lib.rs @@ -28,6 +28,8 @@ pub struct Command<'a> { pub version: &'a str, pub after_options: &'a str, pub args: Vec>, + pub license: &'a str, + pub authors: &'a str, } /// Description of an argument diff --git a/complete/src/man.rs b/complete/src/man.rs index 494f828..046cf08 100644 --- a/complete/src/man.rs +++ b/complete/src/man.rs @@ -57,5 +57,12 @@ pub fn render(c: &Command) -> String { page.text(flags); page.text([roman(arg.help)]); } + + page.control("SH", ["AUTHORS"]); + page.text([roman(c.authors)]); + + page.control("SH", ["COPYRIGHT"]); + page.text([roman(format!("Copyright © {}.", &c.authors))]); + page.text([roman(format!("License: {}", &c.license))]); page.render() } diff --git a/derive/src/complete.rs b/derive/src/complete.rs index 9e9ea96..64f7d05 100644 --- a/derive/src/complete.rs +++ b/derive/src/complete.rs @@ -90,6 +90,8 @@ pub fn complete(args: &[Argument], file: &Option) -> TokenStream { summary: #summary, after_options: #after_options, version: env!("CARGO_PKG_VERSION"), - args: vec![#(#arg_specs),*] + args: vec![#(#arg_specs),*], + license: env!("CARGO_PKG_LICENSE"), + authors: env!("CARGO_PKG_AUTHORS"), }) } From 4d66c60ecfea0e8dd9bbd8ecc54fbdde35be018d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 Dec 2023 01:08:17 +0000 Subject: [PATCH 031/116] fix(deps): update rust crate syn to 2.0.41 --- derive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 4d86544..3f3bc83 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -11,4 +11,4 @@ proc_macro = true [dependencies] proc-macro2 = "1.0.70" quote = "1.0.33" -syn = { version = "2.0.40", features = ["full"] } +syn = { version = "2.0.41", features = ["full"] } From 787d685b363b56843ade43737a29fd820132242e Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Thu, 14 Dec 2023 13:29:51 +0100 Subject: [PATCH 032/116] move helper functions to `internal` module --- derive/src/argument.rs | 19 +++-- derive/src/help.rs | 2 +- derive/src/lib.rs | 2 +- src/internal.rs | 161 +++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 155 +-------------------------------------- 5 files changed, 176 insertions(+), 163 deletions(-) create mode 100644 src/internal.rs diff --git a/derive/src/argument.rs b/derive/src/argument.rs index 7d194f1..a4d9e77 100644 --- a/derive/src/argument.rs +++ b/derive/src/argument.rs @@ -254,7 +254,7 @@ pub fn long_handling(args: &[Argument], help_flags: &Flags) -> TokenStream { quote!( let long_options: [&str; #num_opts] = [#(#options),*]; - let long = ::uutils_args::infer_long_option(long, &long_options)?; + let long = ::uutils_args::internal::infer_long_option(long, &long_options)?; #help_check @@ -284,7 +284,7 @@ pub fn free_handling(args: &[Argument]) -> TokenStream { if_expressions.push(quote!( if let Some(inner) = #filter(arg) { - let value = ::uutils_args::parse_value_for_option("", ::std::ffi::OsStr::new(inner))?; + let value = ::uutils_args::internal::parse_value_for_option("", ::std::ffi::OsStr::new(inner))?; let _ = raw.next(); return Ok(Some(Argument::Custom(Self::#ident(value)))); } @@ -308,7 +308,7 @@ pub fn free_handling(args: &[Argument]) -> TokenStream { dd_args.push(prefix); dd_branches.push(quote!( if prefix == #prefix { - let value = ::uutils_args::parse_value_for_option("", ::std::ffi::OsStr::new(value))?; + let value = ::uutils_args::internal::parse_value_for_option("", ::std::ffi::OsStr::new(value))?; let _ = raw.next(); return Ok(Some(Argument::Custom(Self::#ident(value)))); } @@ -321,7 +321,10 @@ pub fn free_handling(args: &[Argument]) -> TokenStream { if let Some((prefix, value)) = arg.split_once('=') { #(#dd_branches)* - return Err(::uutils_args::Error::UnexpectedOption(prefix.to_string(), ::uutils_args::filter_suggestions(prefix, &[#(#dd_args),*], ""))); + return Err(::uutils_args::Error::UnexpectedOption( + prefix.to_string(), + ::uutils_args::internal::filter_suggestions(prefix, &[#(#dd_args),*], "") + )); } )); } @@ -410,19 +413,19 @@ fn default_value_expression(ident: &Ident, default_expr: &TokenStream) -> TokenS fn optional_value_expression(ident: &Ident, default_expr: &TokenStream) -> TokenStream { quote!(match parser.optional_value() { - Some(value) => Self::#ident(::uutils_args::parse_value_for_option(&option, &value)?), + Some(value) => Self::#ident(::uutils_args::internal::parse_value_for_option(&option, &value)?), None => Self::#ident(#default_expr), }) } fn required_value_expression(ident: &Ident) -> TokenStream { - quote!(Self::#ident(::uutils_args::parse_value_for_option(&option, &parser.value()?)?)) + quote!(Self::#ident(::uutils_args::internal::parse_value_for_option(&option, &parser.value()?)?)) } fn positional_expression(ident: &Ident) -> TokenStream { // TODO: Add option name in this from_value call quote!( - Self::#ident(::uutils_args::parse_value_for_option("", &value)?) + Self::#ident(::uutils_args::internal::parse_value_for_option("", &value)?) ) } @@ -432,7 +435,7 @@ fn last_positional_expression(ident: &Ident) -> TokenStream { let raw_args = parser.raw_args()?; let collection = std::iter::once(value) .chain(raw_args) - .map(|v| ::uutils_args::parse_value_for_option("", &v)) + .map(|v| ::uutils_args::internal::parse_value_for_option("", &v)) .collect::>()?; Self::#ident(collection) }) diff --git a/derive/src/help.rs b/derive/src/help.rs index 9ebeaf2..ad5d5ae 100644 --- a/derive/src/help.rs +++ b/derive/src/help.rs @@ -75,7 +75,7 @@ pub fn help_string( } let options = if !options.is_empty() { - quote!(::uutils_args::print_flags(&mut w, #indent, #width, [#(#options),*])?;) + quote!(::uutils_args::internal::print_flags(&mut w, #indent, #width, [#(#options),*])?;) } else { quote!() }; diff --git a/derive/src/lib.rs b/derive/src/lib.rs index cd8ccbf..5967c09 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -57,7 +57,7 @@ pub fn arguments(input: TokenStream) -> TokenStream { // This is a bit of a hack to support `echo` and should probably not be // used in general. let next_arg = if arguments_attr.parse_echo_style { - quote!(if let Some(val) = uutils_args::__echo_style_positional(parser, &[#(#short_flags),*]) { + quote!(if let Some(val) = ::uutils_args::internal::echo_style_positional(parser, &[#(#short_flags),*]) { Some(lexopt::Arg::Value(val)) } else { parser.next()? diff --git a/src/internal.rs b/src/internal.rs new file mode 100644 index 0000000..8a13d57 --- /dev/null +++ b/src/internal.rs @@ -0,0 +1,161 @@ +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +//! Functions to be used by `uutils-args-derive`. +//! +//! This has the following implications: +//! - These functions are not guaranteed to be stable. +//! - These functions should not be used outside the derive crate +//! +//! Yet, they should be properly documented to make macro-expanded code +//! readable. + +use super::{Error, Value}; +use std::{ + ffi::{OsStr, OsString}, + io::Write, +}; + +/// Parses an echo-style positional argument +/// +/// This means that any argument that does not solely consist of a hyphen +/// followed by the characters in the list of `short_args` is considered +/// to be a positional argument, instead of an invalid argument. This +/// includes the `--` argument, which is ignored by `echo`. +pub fn echo_style_positional(p: &mut lexopt::Parser, short_args: &[char]) -> Option { + let mut raw = p.try_raw_args()?; + let val = raw.peek()?; + + if is_echo_style_positional(val, short_args) { + let val = val.into(); + raw.next(); + Some(val) + } else { + None + } +} + +fn is_echo_style_positional(s: &OsStr, short_args: &[char]) -> bool { + let s = match s.to_str() { + Some(x) => x, + // If it's invalid utf-8 then it can't be a short arg, so must + // be a positional argument. + None => return true, + }; + let mut chars = s.chars(); + let is_short_args = chars.next() == Some('-') && chars.all(|c| short_args.contains(&c)); + !is_short_args +} + +/// Parse an argument defined by a prefix +pub fn parse_prefix(parser: &mut lexopt::Parser, prefix: &'static str) -> Option { + let mut raw = parser.try_raw_args()?; + + // TODO: The to_str call is a limitation. Maybe we need to pull in something like bstr + let arg = raw.peek()?.to_str()?; + let value_str = arg.strip_prefix(prefix)?; + + let value = T::from_value(OsStr::new(value_str)).ok()?; + + // Consume the argument we just parsed + let _ = raw.next(); + + Some(value) +} + +/// Parse a value and wrap the error into an `Error::ParsingFailed` +pub fn parse_value_for_option(opt: &str, v: &OsStr) -> Result { + T::from_value(v).map_err(|e| Error::ParsingFailed { + option: opt.into(), + value: v.to_string_lossy().to_string(), + error: e, + }) +} + +/// Expand unambiguous prefixes to a list of candidates +pub fn infer_long_option<'a>( + input: &'a str, + long_options: &'a [&'a str], +) -> Result<&'a str, Error> { + let mut candidates = Vec::new(); + let mut exact_match = None; + for opt in long_options { + if *opt == input { + exact_match = Some(opt); + break; + } else if opt.starts_with(input) { + candidates.push(opt); + } + } + + match (exact_match, &candidates[..]) { + (Some(opt), _) => Ok(*opt), + (None, [opt]) => Ok(**opt), + (None, []) => Err(Error::UnexpectedOption( + format!("--{input}"), + filter_suggestions(input, long_options, "--"), + )), + (None, _) => Err(Error::AmbiguousOption { + option: input.to_string(), + candidates: candidates.iter().map(|s| s.to_string()).collect(), + }), + } +} + +/// Filter a list of options to just the elements that are similar to the given string +pub fn filter_suggestions(input: &str, long_options: &[&str], prefix: &str) -> Vec { + long_options + .iter() + .filter(|opt| strsim::jaro(input, opt) > 0.7) + .map(|o| format!("{prefix}{o}")) + .collect() +} + +/// Print a formatted list of options. +pub fn print_flags( + mut w: impl Write, + indent_size: usize, + width: usize, + options: impl IntoIterator, +) -> std::io::Result<()> { + let indent = " ".repeat(indent_size); + writeln!(w, "\nOptions:")?; + for (flags, help_string) in options { + let mut help_lines = help_string.lines(); + write!(w, "{}{}", &indent, &flags)?; + + if flags.len() <= width { + let line = match help_lines.next() { + Some(line) => line, + None => { + writeln!(w)?; + continue; + } + }; + let help_indent = " ".repeat(width - flags.len() + 2); + writeln!(w, "{}{}", help_indent, line)?; + } else { + writeln!(w)?; + } + + let help_indent = " ".repeat(width + indent_size + 2); + for line in help_lines { + writeln!(w, "{}{}", help_indent, line)?; + } + } + Ok(()) +} + +#[cfg(test)] +mod test { + use std::ffi::OsStr; + + use super::is_echo_style_positional; + + #[test] + fn echo_positional() { + assert!(is_echo_style_positional(OsStr::new("-aaa"), &['b'])); + assert!(is_echo_style_positional(OsStr::new("--"), &['b'])); + assert!(!is_echo_style_positional(OsStr::new("-b"), &['b'])); + } +} diff --git a/src/lib.rs b/src/lib.rs index 5ec42d2..d259d9d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ #![doc = include_str!("../README.md")] mod error; +pub mod internal; mod value; pub use lexopt; @@ -12,11 +13,7 @@ pub use uutils_args_derive::*; pub use error::Error; pub use value::{Value, ValueError, ValueResult}; -use std::{ - ffi::{OsStr, OsString}, - io::Write, - marker::PhantomData, -}; +use std::{ffi::OsString, marker::PhantomData}; /// A wrapper around a type implementing [`Arguments`] that adds `Help` /// and `Version` variants. @@ -225,29 +222,6 @@ pub trait Options: Sized { } } -/// Parses an echo-style positional argument -/// -/// This means that any argument that does not solely consist of a hyphen -/// followed by the characters in the list of `short_args` is considered -/// to be a positional argument, instead of an invalid argument. This -/// includes the `--` argument, which is ignored by `echo`. -/// -/// This function is hidden and prefixed with `__` because it should only -/// be called via the derive macros. -#[doc(hidden)] -pub fn __echo_style_positional(p: &mut lexopt::Parser, short_args: &[char]) -> Option { - let mut raw = p.try_raw_args()?; - let val = raw.peek()?; - - if is_echo_style_positional(val, short_args) { - let val = val.into(); - raw.next(); - Some(val) - } else { - None - } -} - #[cfg(feature = "parse-is-complete")] fn print_complete, Arg: Arguments>(mut args: I) where @@ -263,128 +237,3 @@ where assert!(args.next().is_none(), "completion only takes one argument"); println!("{}", O::complete(&shell)); } - -fn is_echo_style_positional(s: &OsStr, short_args: &[char]) -> bool { - let s = match s.to_str() { - Some(x) => x, - // If it's invalid utf-8 then it can't be a short arg, so must - // be a positional argument. - None => return true, - }; - let mut chars = s.chars(); - let is_short_args = chars.next() == Some('-') && chars.all(|c| short_args.contains(&c)); - !is_short_args -} - -/// Parse an argument defined by a prefix -#[doc(hidden)] -pub fn parse_prefix(parser: &mut lexopt::Parser, prefix: &'static str) -> Option { - let mut raw = parser.try_raw_args()?; - - // TODO: The to_str call is a limitation. Maybe we need to pull in something like bstr - let arg = raw.peek()?.to_str()?; - let value_str = arg.strip_prefix(prefix)?; - - let value = T::from_value(OsStr::new(value_str)).ok()?; - - // Consume the argument we just parsed - let _ = raw.next(); - - Some(value) -} - -/// Parse a value and wrap the error into an `Error::ParsingFailed` -#[doc(hidden)] -pub fn parse_value_for_option(opt: &str, v: &OsStr) -> Result { - T::from_value(v).map_err(|e| Error::ParsingFailed { - option: opt.into(), - value: v.to_string_lossy().to_string(), - error: e, - }) -} - -pub fn infer_long_option<'a>( - input: &'a str, - long_options: &'a [&'a str], -) -> Result<&'a str, Error> { - let mut candidates = Vec::new(); - let mut exact_match = None; - for opt in long_options { - if *opt == input { - exact_match = Some(opt); - break; - } else if opt.starts_with(input) { - candidates.push(opt); - } - } - - match (exact_match, &candidates[..]) { - (Some(opt), _) => Ok(*opt), - (None, [opt]) => Ok(**opt), - (None, []) => Err(Error::UnexpectedOption( - format!("--{input}"), - filter_suggestions(input, long_options, "--"), - )), - (None, _) => Err(Error::AmbiguousOption { - option: input.to_string(), - candidates: candidates.iter().map(|s| s.to_string()).collect(), - }), - } -} - -/// Filter a list of options to just the elements that are similar to the given string -pub fn filter_suggestions(input: &str, long_options: &[&str], prefix: &str) -> Vec { - long_options - .iter() - .filter(|opt| strsim::jaro(input, opt) > 0.7) - .map(|o| format!("{prefix}{o}")) - .collect() -} - -pub fn print_flags( - mut w: impl Write, - indent_size: usize, - width: usize, - options: impl IntoIterator, -) -> std::io::Result<()> { - let indent = " ".repeat(indent_size); - writeln!(w, "\nOptions:")?; - for (flags, help_string) in options { - let mut help_lines = help_string.lines(); - write!(w, "{}{}", &indent, &flags)?; - - if flags.len() <= width { - let line = match help_lines.next() { - Some(line) => line, - None => { - writeln!(w)?; - continue; - } - }; - let help_indent = " ".repeat(width - flags.len() + 2); - writeln!(w, "{}{}", help_indent, line)?; - } else { - writeln!(w)?; - } - - let help_indent = " ".repeat(width + indent_size + 2); - for line in help_lines { - writeln!(w, "{}{}", help_indent, line)?; - } - } - Ok(()) -} - -#[cfg(test)] -mod test { - use std::ffi::OsStr; - - use crate::is_echo_style_positional; - - #[test] - fn echo_positional() { - assert!(is_echo_style_positional(OsStr::new("-aaa"), &['b'])); - assert!(is_echo_style_positional(OsStr::new("--"), &['b'])); - assert!(!is_echo_style_positional(OsStr::new("-b"), &['b'])); - } -} From affc4b6c6ae33fa1421265bab13d88ceffbd7c52 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Fri, 15 Dec 2023 15:15:31 +0100 Subject: [PATCH 033/116] add exit code to the error type --- derive/src/argument.rs | 15 +++++++++------ derive/src/lib.rs | 2 +- src/error.rs | 41 ++++++++++++++++++++++++++--------------- src/internal.rs | 13 +++++++------ src/lib.rs | 21 +++++++++++++-------- src/value.rs | 10 +++++++--- 6 files changed, 63 insertions(+), 39 deletions(-) diff --git a/derive/src/argument.rs b/derive/src/argument.rs index a4d9e77..c6b2f2d 100644 --- a/derive/src/argument.rs +++ b/derive/src/argument.rs @@ -186,7 +186,7 @@ pub fn short_handling(args: &[Argument]) -> (TokenStream, Vec) { Ok(Some(Argument::Custom( match short { #(#match_arms)* - _ => return Err(::uutils_args::Error::UnexpectedOption(short.to_string(), Vec::new())), + _ => return Err(::uutils_args::ErrorKind::UnexpectedOption(short.to_string(), Vec::new())), } ))) ); @@ -233,7 +233,7 @@ pub fn long_handling(args: &[Argument], help_flags: &Flags) -> TokenStream { if options.is_empty() { return quote!( - return Err(::uutils_args::Error::UnexpectedOption( + return Err(::uutils_args::ErrorKind::UnexpectedOption( long.to_string(), Vec::new() )) @@ -321,7 +321,7 @@ pub fn free_handling(args: &[Argument]) -> TokenStream { if let Some((prefix, value)) = arg.split_once('=') { #(#dd_branches)* - return Err(::uutils_args::Error::UnexpectedOption( + return Err(::uutils_args::ErrorKind::UnexpectedOption( prefix.to_string(), ::uutils_args::internal::filter_suggestions(prefix, &[#(#dd_args),*], "") )); @@ -392,9 +392,12 @@ pub fn positional_handling(args: &[Argument]) -> (TokenStream, TokenStream) { let mut missing: Vec<&str> = vec![]; #(#missing_argument_checks)* if !missing.is_empty() { - Err(uutils_args::Error::MissingPositionalArguments( - missing.iter().map(ToString::to_string).collect::>() - )) + Err(uutils_args::Error { + exit_code: Self::EXIT_CODE, + kind: uutils_args::ErrorKind::MissingPositionalArguments( + missing.iter().map(ToString::to_string).collect::>() + ) + }) } else { Ok(()) } diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 5967c09..bfeb314 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -73,7 +73,7 @@ pub fn arguments(input: TokenStream) -> TokenStream { #[allow(unreachable_code)] fn next_arg( parser: &mut uutils_args::lexopt::Parser, positional_idx: &mut usize - ) -> Result>, uutils_args::Error> { + ) -> Result>, ::uutils_args::ErrorKind> { use uutils_args::{Value, lexopt, Error, Argument}; #free diff --git a/src/error.rs b/src/error.rs index e2c9faa..ff7a6f4 100644 --- a/src/error.rs +++ b/src/error.rs @@ -7,8 +7,13 @@ use std::{ fmt::{Debug, Display}, }; +pub struct Error { + pub exit_code: i32, + pub kind: ErrorKind, +} + /// Errors that can occur while parsing arguments. -pub enum Error { +pub enum ErrorKind { /// There was an option that required an option, but none was given. MissingValue { option: Option, @@ -51,53 +56,59 @@ pub enum Error { IoError(std::io::Error), } -impl From for Error { +impl From for ErrorKind { fn from(value: std::io::Error) -> Self { - Error::IoError(value) + ErrorKind::IoError(value) } } impl StdError for Error {} +impl Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.kind.fmt(f) + } +} + impl Debug for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Display::fmt(self, f) } } -impl Display for Error { +impl Display for ErrorKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "error: ")?; match self { - Error::MissingValue { option } => match option { + ErrorKind::MissingValue { option } => match option { Some(option) => write!(f, "Missing value for '{option}'."), None => write!(f, "Missing value"), }, - Error::MissingPositionalArguments(args) => { + ErrorKind::MissingPositionalArguments(args) => { write!(f, "Missing values for the following positional arguments:")?; for arg in args { write!(f, " - {arg}")?; } Ok(()) } - Error::UnexpectedOption(opt, suggestions) => { + ErrorKind::UnexpectedOption(opt, suggestions) => { write!(f, "Found an invalid option '{opt}'.")?; if !suggestions.is_empty() { write!(f, "\nDid you mean: {}", suggestions.join(", "))?; } Ok(()) } - Error::UnexpectedArgument(arg) => { + ErrorKind::UnexpectedArgument(arg) => { write!(f, "Found an invalid argument '{}'.", arg.to_string_lossy()) } - Error::UnexpectedValue { option, value } => { + ErrorKind::UnexpectedValue { option, value } => { write!( f, "Got an unexpected value '{}' for option '{option}'.", value.to_string_lossy(), ) } - Error::ParsingFailed { + ErrorKind::ParsingFailed { option, value, error, @@ -110,7 +121,7 @@ impl Display for Error { write!(f, "Invalid value '{value}' for '{option}': {error}") } } - Error::AmbiguousOption { option, candidates } => { + ErrorKind::AmbiguousOption { option, candidates } => { write!( f, "Option '{option}' is ambiguous. The following candidates match:" @@ -120,16 +131,16 @@ impl Display for Error { } Ok(()) } - Error::NonUnicodeValue(x) => { + ErrorKind::NonUnicodeValue(x) => { write!(f, "Invalid unicode value found: {}", x.to_string_lossy()) } - Error::IoError(x) => std::fmt::Display::fmt(x, f), + ErrorKind::IoError(x) => std::fmt::Display::fmt(x, f), } } } -impl From for Error { - fn from(other: lexopt::Error) -> Error { +impl From for ErrorKind { + fn from(other: lexopt::Error) -> ErrorKind { match other { lexopt::Error::MissingValue { option } => Self::MissingValue { option }, lexopt::Error::UnexpectedOption(s) => Self::UnexpectedOption(s, Vec::new()), diff --git a/src/internal.rs b/src/internal.rs index 8a13d57..412c737 100644 --- a/src/internal.rs +++ b/src/internal.rs @@ -10,7 +10,8 @@ //! Yet, they should be properly documented to make macro-expanded code //! readable. -use super::{Error, Value}; +use crate::error::ErrorKind; +use crate::value::Value; use std::{ ffi::{OsStr, OsString}, io::Write, @@ -64,8 +65,8 @@ pub fn parse_prefix(parser: &mut lexopt::Parser, prefix: &'static str) } /// Parse a value and wrap the error into an `Error::ParsingFailed` -pub fn parse_value_for_option(opt: &str, v: &OsStr) -> Result { - T::from_value(v).map_err(|e| Error::ParsingFailed { +pub fn parse_value_for_option(opt: &str, v: &OsStr) -> Result { + T::from_value(v).map_err(|e| ErrorKind::ParsingFailed { option: opt.into(), value: v.to_string_lossy().to_string(), error: e, @@ -76,7 +77,7 @@ pub fn parse_value_for_option(opt: &str, v: &OsStr) -> Result( input: &'a str, long_options: &'a [&'a str], -) -> Result<&'a str, Error> { +) -> Result<&'a str, ErrorKind> { let mut candidates = Vec::new(); let mut exact_match = None; for opt in long_options { @@ -91,11 +92,11 @@ pub fn infer_long_option<'a>( match (exact_match, &candidates[..]) { (Some(opt), _) => Ok(*opt), (None, [opt]) => Ok(**opt), - (None, []) => Err(Error::UnexpectedOption( + (None, []) => Err(ErrorKind::UnexpectedOption( format!("--{input}"), filter_suggestions(input, long_options, "--"), )), - (None, _) => Err(Error::AmbiguousOption { + (None, _) => Err(ErrorKind::AmbiguousOption { option: input.to_string(), candidates: candidates.iter().map(|s| s.to_string()).collect(), }), diff --git a/src/lib.rs b/src/lib.rs index d259d9d..08bb6ed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,7 @@ mod value; pub use lexopt; pub use uutils_args_derive::*; -pub use error::Error; +pub use error::{Error, ErrorKind}; pub use value::{Value, ValueError, ValueResult}; use std::{ffi::OsString, marker::PhantomData}; @@ -24,12 +24,12 @@ pub enum Argument { Custom(T), } -fn exit_if_err(res: Result, exit_code: i32) -> T { +fn exit_if_err(res: Result) -> T { match res { Ok(v) => v, Err(err) => { eprintln!("{err}"); - std::process::exit(exit_code); + std::process::exit(err.exit_code); } } } @@ -62,7 +62,7 @@ pub trait Arguments: Sized { fn next_arg( parser: &mut lexopt::Parser, positional_idx: &mut usize, - ) -> Result>, Error>; + ) -> Result>, ErrorKind>; /// Check for any required arguments that have not been found. /// @@ -89,7 +89,7 @@ pub trait Arguments: Sized { I: IntoIterator + 'static, I::Item: Into, { - exit_if_err(Self::try_check(args), Self::EXIT_CODE) + exit_if_err(Self::try_check(args)) } /// Check all arguments immediately and return any errors. @@ -135,10 +135,15 @@ impl ArgumentIter { } pub fn next_arg(&mut self) -> Result, Error> { - if let Some(arg) = T::next_arg(&mut self.parser, &mut self.positional_idx)? { + if let Some(arg) = + T::next_arg(&mut self.parser, &mut self.positional_idx).map_err(|kind| Error { + exit_code: T::EXIT_CODE, + kind, + })? + { match arg { Argument::Help => { - self.help()?; + self.help().unwrap(); std::process::exit(0); } Argument::Version => { @@ -184,7 +189,7 @@ pub trait Options: Sized { I: IntoIterator + 'static, I::Item: Into, { - exit_if_err(self.try_parse(args), Arg::EXIT_CODE) + exit_if_err(self.try_parse(args)) } #[allow(unused_mut)] diff --git a/src/value.rs b/src/value.rs index ae9e3f7..54cfbba 100644 --- a/src/value.rs +++ b/src/value.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::error::Error; +use crate::error::{Error, ErrorKind}; use std::{ ffi::{OsStr, OsString}, path::PathBuf, @@ -50,7 +50,7 @@ impl std::fmt::Display for ValueError { /// Defines how a type should be parsed from an argument. /// -/// If an error is returned, it will be wrapped in [`Error::ParsingFailed`] +/// If an error is returned, it will be wrapped in [`ErrorKind::ParsingFailed`] pub trait Value: Sized { fn from_value(value: &OsStr) -> ValueResult; @@ -81,7 +81,11 @@ impl Value for String { fn from_value(value: &OsStr) -> ValueResult { match value.to_str() { Some(s) => Ok(s.into()), - None => Err(Error::NonUnicodeValue(value.into()).into()), + None => Err(Error { + exit_code: 1, + kind: ErrorKind::NonUnicodeValue(value.into()), + } + .into()), } } } From cc9ce1ea508bd58bf1cd2af7d1de09bbf83f48cb Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Fri, 15 Dec 2023 23:29:25 +0100 Subject: [PATCH 034/116] remove 'static lifetime This is probably an old remnant, but it is no longer necessary --- README.md | 2 +- src/lib.rs | 14 +++++++------- tests/coreutils/basename.rs | 2 +- tests/coreutils/head.rs | 4 ++-- tests/coreutils/tail.rs | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 7119bb7..48ecec8 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ impl Options for Settings { } } -fn run(args: &'static [&'static str]) -> String { +fn run(args: &[&str]) -> String { let s = Settings::default().parse(args); let mut output = if s.caps { s.text.to_uppercase() diff --git a/src/lib.rs b/src/lib.rs index d259d9d..e098140 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,7 +50,7 @@ pub trait Arguments: Sized { /// [`ArgumentIter`](ArgumentIter). fn parse(args: I) -> ArgumentIter where - I: IntoIterator + 'static, + I: IntoIterator, I::Item: Into, { ArgumentIter::::from_args(args) @@ -86,7 +86,7 @@ pub trait Arguments: Sized { /// exit if `--help` or `--version` are passed and if any errors are found. fn check(args: I) where - I: IntoIterator + 'static, + I: IntoIterator, I::Item: Into, { exit_if_err(Self::try_check(args), Self::EXIT_CODE) @@ -98,7 +98,7 @@ pub trait Arguments: Sized { /// exit if `--help` or `--version` are passed. fn try_check(args: I) -> Result<(), Error> where - I: IntoIterator + 'static, + I: IntoIterator, I::Item: Into, { let mut iter = Self::parse(args); @@ -124,7 +124,7 @@ pub struct ArgumentIter { impl ArgumentIter { fn from_args(args: I) -> Self where - I: IntoIterator + 'static, + I: IntoIterator, I::Item: Into, { Self { @@ -181,7 +181,7 @@ pub trait Options: Sized { /// Parse an iterator of arguments into the options fn parse(self, args: I) -> Self where - I: IntoIterator + 'static, + I: IntoIterator, I::Item: Into, { exit_if_err(self.try_parse(args), Arg::EXIT_CODE) @@ -190,7 +190,7 @@ pub trait Options: Sized { #[allow(unused_mut)] fn try_parse(mut self, args: I) -> Result where - I: IntoIterator + 'static, + I: IntoIterator, I::Item: Into, { // Hacky but it works: if the parse-is-complete flag is active the @@ -225,7 +225,7 @@ pub trait Options: Sized { #[cfg(feature = "parse-is-complete")] fn print_complete, Arg: Arguments>(mut args: I) where - I: Iterator + 'static, + I: Iterator, I::Item: Into, { let _exec_name = args.next(); diff --git a/tests/coreutils/basename.rs b/tests/coreutils/basename.rs index 7ceaa33..66d598a 100644 --- a/tests/coreutils/basename.rs +++ b/tests/coreutils/basename.rs @@ -37,7 +37,7 @@ impl Options for Settings { } } -fn parse(args: &'static [&'static str]) -> Settings { +fn parse(args: &[&str]) -> Settings { let mut settings = Settings::default().parse(args); if !settings.multiple { assert_eq!(settings.names.len(), 2); diff --git a/tests/coreutils/head.rs b/tests/coreutils/head.rs index 40f3e81..c1257c7 100644 --- a/tests/coreutils/head.rs +++ b/tests/coreutils/head.rs @@ -10,7 +10,7 @@ use uutils_args::{Arguments, Options, Value}; // optional at compile time. As the GNU docs explain, it's very error-prone. fn parse_deprecated(iter: I) -> Option where - I: IntoIterator + Clone + 'static, + I: IntoIterator + Clone, I::Item: Into, { let mut iter = iter.into_iter(); @@ -210,7 +210,7 @@ impl Options for Settings { fn parse_head(iter: I) -> Result where - I: IntoIterator + Clone + 'static, + I: IntoIterator + Clone, I::Item: Into, { match parse_deprecated(iter.clone()) { diff --git a/tests/coreutils/tail.rs b/tests/coreutils/tail.rs index 671b9be..7e6b63e 100644 --- a/tests/coreutils/tail.rs +++ b/tests/coreutils/tail.rs @@ -10,7 +10,7 @@ use uutils_args::{Arguments, Options, Value}; // optional at compile time. As the GNU docs explain, it's very error-prone. fn parse_deprecated(iter: I) -> Option where - I: IntoIterator + Clone + 'static, + I: IntoIterator + Clone, I::Item: Into, { let mut iter = iter.into_iter(); @@ -275,7 +275,7 @@ impl Options for Settings { fn parse_tail(iter: I) -> Result where - I: IntoIterator + Clone + 'static, + I: IntoIterator + Clone, I::Item: Into, { match parse_deprecated(iter.clone()) { From 462898ca91fa0e2cdf474d2e790fa14faa421242 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Thu, 14 Dec 2023 12:28:24 +0100 Subject: [PATCH 035/116] take positional arguments out of arguments enum --- .github/workflows/ci.yml | 2 +- README.md | 38 +++------ derive/src/argument.rs | 104 ----------------------- derive/src/attributes.rs | 84 +------------------ derive/src/help.rs | 1 - derive/src/lib.rs | 15 +--- examples/deprecated.rs | 6 +- examples/hello_world.rs | 2 +- examples/value.rs | 4 +- src/lib.rs | 51 +++++------ tests/coreutils/arch.rs | 1 - tests/coreutils/b2sum.rs | 42 ++++++---- tests/coreutils/base32.rs | 16 +--- tests/coreutils/basename.rs | 15 ++-- tests/coreutils/cat.rs | 23 ++--- tests/coreutils/dd.rs | 21 ++--- tests/coreutils/echo.rs | 18 ++-- tests/coreutils/head.rs | 53 ++++++------ tests/coreutils/ls.rs | 54 +++++------- tests/coreutils/mktemp.rs | 23 ++--- tests/coreutils/tail.rs | 72 ++++++++-------- tests/coreutils/uniq.rs | 22 ++--- tests/flags.rs | 72 ++++++++-------- tests/options.rs | 73 ++++++++-------- tests/positionals.rs | 163 ------------------------------------ 25 files changed, 285 insertions(+), 690 deletions(-) delete mode 100644 tests/positionals.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c03e0c1..d3e5f46 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,7 +57,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: clippy - args: --all-targets --all-features --workspace -- -D warnings + args: --all-targets --features complete --workspace -- -D warnings docs: name: Docs diff --git a/README.md b/README.md index 48ecec8..839a510 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,13 @@ behaviour that aligns with GNU coreutils. ## Features - - A derive macro for declarative argument definition. - - Automatic help generation. - - Positional and optional arguments. - - Automatically parsing values into Rust types. - - Define a custom exit code on errors. - - Automatically accept unambiguous abbreviations of long options. - - Handles invalid UTF-8 gracefully. +- A derive macro for declarative argument definition. +- Automatic help generation. +- Positional and optional arguments. +- Automatically parsing values into Rust types. +- Define a custom exit code on errors. +- Automatically accept unambiguous abbreviations of long options. +- Handles invalid UTF-8 gracefully. ## When you should not use this library @@ -50,16 +50,11 @@ enum Arg { /// Transform input text to uppercase #[arg("-c", "--caps")] Caps, - - // This option takes a value: + + // This option takes a value: /// Add exclamation marks to output #[arg("-e N", "--exclaim=N")] ExclamationMarks(u8), - - // This is a positional argument, the range specifies that - // at least one positional argument must be passed. - #[arg("TEXT", 1..)] - Text(String), } #[derive(Default)] @@ -76,24 +71,17 @@ impl Options for Settings { match arg { Arg::Caps => self.caps = true, Arg::ExclamationMarks(n) => self.exclamation_marks += n, - Arg::Text(s) => { - if self.text.is_empty() { - self.text.push_str(&s); - } else { - self.text.push(' '); - self.text.push_str(&s); - } - } } } } fn run(args: &[&str]) -> String { - let s = Settings::default().parse(args); + let (s, operands) = Settings::default().parse(args); + let text = operands.iter().map(|s| s.to_string_lossy()).collect::>().join(" "); let mut output = if s.caps { - s.text.to_uppercase() + text.to_uppercase() } else { - s.text + text }; for i in 0..s.exclamation_marks { output.push('!'); diff --git a/derive/src/argument.rs b/derive/src/argument.rs index c6b2f2d..ddb4a5a 100644 --- a/derive/src/argument.rs +++ b/derive/src/argument.rs @@ -1,8 +1,6 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use std::ops::RangeInclusive; - use proc_macro2::TokenStream; use quote::quote; use syn::{Attribute, Fields, FieldsUnnamed, Ident, Meta, Variant}; @@ -27,10 +25,6 @@ pub enum ArgType { takes_value: bool, default: TokenStream, }, - Positional { - num_args: RangeInclusive, - last: bool, - }, Free { filters: Vec, }, @@ -93,13 +87,6 @@ pub fn parse_argument(v: Variant) -> Vec { hidden: opt.hidden, } } - ArgAttr::Positional(pos) => { - assert!(field.is_some(), "Positional arguments must have a field"); - ArgType::Positional { - num_args: pos.num_args, - last: pos.last, - } - } ArgAttr::Free(free) => ArgType::Free { filters: free.filters, }, @@ -157,7 +144,6 @@ pub fn short_handling(args: &[Argument]) -> (TokenStream, Vec) { ref default, hidden: _, } => (flags, takes_value, default), - ArgType::Positional { .. } => continue, ArgType::Free { .. } => continue, }; @@ -207,7 +193,6 @@ pub fn long_handling(args: &[Argument], help_flags: &Flags) -> TokenStream { ref default, hidden: _, } => (flags, takes_value, default), - ArgType::Positional { .. } => continue, ArgType::Free { .. } => continue, }; @@ -275,7 +260,6 @@ pub fn free_handling(args: &[Argument]) -> TokenStream { for arg @ Argument { arg_type, .. } in args { let filters = match arg_type { ArgType::Free { filters } => filters, - ArgType::Positional { .. } => continue, ArgType::Option { .. } => continue, }; @@ -299,7 +283,6 @@ pub fn free_handling(args: &[Argument]) -> TokenStream { let flags = match arg_type { ArgType::Option { flags, .. } => flags, ArgType::Free { .. } => continue, - ArgType::Positional { .. } => continue, }; for (prefix, _) in &flags.dd_style { @@ -338,74 +321,6 @@ pub fn free_handling(args: &[Argument]) -> TokenStream { ) } -pub fn positional_handling(args: &[Argument]) -> (TokenStream, TokenStream) { - let mut match_arms = Vec::new(); - // The largest index of the previous argument, so the the argument after this should - // belong to the next argument. - let mut last_index = 0; - - // The minimum number of arguments needed to not return a missing argument error. - let mut minimum_needed = 0; - let mut missing_argument_checks = vec![]; - - for arg @ Argument { name, arg_type, .. } in args { - let (num_args, last) = match arg_type { - ArgType::Positional { num_args, last } => (num_args, last), - ArgType::Option { .. } => continue, - ArgType::Free { .. } => continue, - }; - - if *num_args.start() > 0 { - minimum_needed = last_index + num_args.start(); - missing_argument_checks.push(quote!(if positional_idx < #minimum_needed { - missing.push(#name); - })); - } - - last_index += num_args.end(); - - let expr = if *last { - last_positional_expression(&arg.ident) - } else { - positional_expression(&arg.ident) - }; - match_arms.push(quote!(0..=#last_index => { #expr })); - } - - let value_handling = quote!( - *positional_idx += 1; - Ok(Some(Argument::Custom( - match positional_idx { - #(#match_arms)* - _ => return Err(lexopt::Arg::Value(value).unexpected().into()), - } - ))) - ); - - let missing_argument_checks = quote!( - // We have the minimum number of required arguments overall. - // So we don't need to check the others. - if positional_idx >= #minimum_needed { - return Ok(()); - } - - let mut missing: Vec<&str> = vec![]; - #(#missing_argument_checks)* - if !missing.is_empty() { - Err(uutils_args::Error { - exit_code: Self::EXIT_CODE, - kind: uutils_args::ErrorKind::MissingPositionalArguments( - missing.iter().map(ToString::to_string).collect::>() - ) - }) - } else { - Ok(()) - } - ); - - (value_handling, missing_argument_checks) -} - fn no_value_expression(ident: &Ident) -> TokenStream { quote!(Self::#ident) } @@ -424,22 +339,3 @@ fn optional_value_expression(ident: &Ident, default_expr: &TokenStream) -> Token fn required_value_expression(ident: &Ident) -> TokenStream { quote!(Self::#ident(::uutils_args::internal::parse_value_for_option(&option, &parser.value()?)?)) } - -fn positional_expression(ident: &Ident) -> TokenStream { - // TODO: Add option name in this from_value call - quote!( - Self::#ident(::uutils_args::internal::parse_value_for_option("", &value)?) - ) -} - -fn last_positional_expression(ident: &Ident) -> TokenStream { - // TODO: Add option name in this from_value call - quote!({ - let raw_args = parser.raw_args()?; - let collection = std::iter::once(value) - .chain(raw_args) - .map(|v| ::uutils_args::internal::parse_value_for_option("", &v)) - .collect::>()?; - Self::#ident(collection) - }) -} diff --git a/derive/src/attributes.rs b/derive/src/attributes.rs index 1a77353..fa068b4 100644 --- a/derive/src/attributes.rs +++ b/derive/src/attributes.rs @@ -1,11 +1,8 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use std::ops::RangeInclusive; - use syn::{ - meta::ParseNestedMeta, parse::ParseStream, Attribute, Expr, ExprLit, ExprRange, Ident, Lit, - LitInt, LitStr, RangeLimits, Token, + meta::ParseNestedMeta, parse::ParseStream, Attribute, Expr, Ident, LitInt, LitStr, Token, }; use crate::flags::Flags; @@ -69,7 +66,6 @@ impl ArgumentsAttr { pub enum ArgAttr { Option(OptionAttr), - Positional(PositionalAttr), Free(FreeAttr), } @@ -84,7 +80,7 @@ impl ArgAttr { if v.starts_with('-') || v.contains('=') { OptionAttr::from_args(v, s).map(Self::Option) } else { - PositionalAttr::from_args(v, s).map(Self::Positional) + panic!("Could not determine type of argument"); } } else if let Ok(v) = s.parse::() { FreeAttr::from_args(v, s).map(Self::Free) @@ -170,82 +166,6 @@ impl FreeAttr { } } -pub struct PositionalAttr { - pub num_args: RangeInclusive, - pub last: bool, -} - -impl Default for PositionalAttr { - fn default() -> Self { - Self { - num_args: 1..=1, - last: false, - } - } -} - -impl PositionalAttr { - pub fn from_args(_first_value: String, s: ParseStream) -> syn::Result { - let mut positional_attr = Self::default(); - parse_args(s, |s| { - if (s.peek(LitInt) && s.peek2(Token![..])) || s.peek(Token![..]) { - let range = s.parse::()?; - // We're dealing with a range - let from = match range.start.as_deref() { - Some(Expr::Lit(ExprLit { - lit: Lit::Int(i), .. - })) => i.base10_parse::().unwrap(), - None => 0, - _ => panic!("Range must consist of usize"), - }; - - let inclusive = matches!(range.limits, RangeLimits::Closed(_)); - let to = match range.end.as_deref() { - Some(Expr::Lit(ExprLit { - lit: Lit::Int(i), .. - })) => { - let n = i.base10_parse::().unwrap(); - if inclusive { - Some(n) - } else { - Some(n - 1) - } - } - None => None, - _ => panic!("Range must consist of usize"), - }; - - positional_attr.num_args = match to { - Some(to) => from..=to, - None => from..=usize::MAX, - }; - return Ok(()); - } - - if let Ok(int) = s.parse::() { - let suffix = int.suffix(); - // FIXME: should be a proper error instead of assert! - assert!( - suffix.is_empty() || suffix == "usize", - "The position index must be usize" - ); - let n = int.base10_parse::().unwrap(); - positional_attr.num_args = n..=n; - return Ok(()); - } - - let ident = s.parse::()?; - match ident.to_string().as_str() { - "last" => positional_attr.last = true, - _ => return Err(s.error("unrecognized keyword in value attribute")), - } - Ok(()) - })?; - - Ok(positional_attr) - } -} - #[derive(Default)] pub struct ValueAttr { pub keys: Vec, diff --git a/derive/src/help.rs b/derive/src/help.rs index ad5d5ae..a606ecf 100644 --- a/derive/src/help.rs +++ b/derive/src/help.rs @@ -51,7 +51,6 @@ pub fn help_string( } // Hidden arguments should not show up in --help ArgType::Option { hidden: true, .. } => {} - ArgType::Positional { .. } => {} // TODO: Free arguments should show up in help ArgType::Free { .. } => {} } diff --git a/derive/src/lib.rs b/derive/src/lib.rs index bfeb314..4842374 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -9,8 +9,7 @@ mod help; mod help_parser; use argument::{ - free_handling, long_handling, parse_argument, parse_arguments_attr, positional_handling, - short_handling, + free_handling, long_handling, parse_argument, parse_arguments_attr, short_handling, }; use attributes::ValueAttr; use help::{help_handling, help_string, version_handling}; @@ -36,9 +35,7 @@ pub fn arguments(input: TokenStream) -> TokenStream { let exit_code = arguments_attr.exit_code; let (short, short_flags) = short_handling(&arguments); let long = long_handling(&arguments, &arguments_attr.help_flags); - // let number_argument = number_handling(&arguments); let free = free_handling(&arguments); - let (positional, missing_argument_checks) = positional_handling(&arguments); let help_string = help_string( &arguments, &arguments_attr.help_flags, @@ -72,9 +69,9 @@ pub fn arguments(input: TokenStream) -> TokenStream { #[allow(unreachable_code)] fn next_arg( - parser: &mut uutils_args::lexopt::Parser, positional_idx: &mut usize + parser: &mut ::uutils_args::lexopt::Parser ) -> Result>, ::uutils_args::ErrorKind> { - use uutils_args::{Value, lexopt, Error, Argument}; + use ::uutils_args::{Value, lexopt, Error, Argument}; #free @@ -90,14 +87,10 @@ pub fn arguments(input: TokenStream) -> TokenStream { match arg { lexopt::Arg::Short(short) => { #short }, lexopt::Arg::Long(long) => { #long }, - lexopt::Arg::Value(value) => { #positional }, + lexopt::Arg::Value(value) => { Ok(Some(::uutils_args::Argument::Positional(value))) }, } } - fn check_missing(positional_idx: usize) -> Result<(), uutils_args::Error> { - #missing_argument_checks - } - fn help(bin_name: &str) -> ::std::io::Result<()> { #help_string } diff --git a/examples/deprecated.rs b/examples/deprecated.rs index 3e68ba2..25484b6 100644 --- a/examples/deprecated.rs +++ b/examples/deprecated.rs @@ -43,8 +43,8 @@ impl Options for Settings { } fn main() { - assert_eq!(Settings::default().parse(["test", "-10"]).n1, 10usize); + assert_eq!(Settings::default().parse(["test", "-10"]).0.n1, 10usize); assert!(Settings::default().try_parse(["test", "--10"]).is_err()); - assert_eq!(Settings::default().parse(["test", "+10"]).n2, 10isize); - assert_eq!(Settings::default().parse(["test", "+-10"]).n2, -10isize); + assert_eq!(Settings::default().parse(["test", "+10"]).0.n2, 10isize); + assert_eq!(Settings::default().parse(["test", "+-10"]).0.n2, -10isize); } diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 1adf4b1..a8eb551 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -32,7 +32,7 @@ impl Options for Settings { } fn main() -> Result<(), uutils_args::Error> { - let settings = Settings { + let (settings, _operands) = Settings { name: String::new(), count: 1, } diff --git a/examples/value.rs b/examples/value.rs index a0139f3..997d136 100644 --- a/examples/value.rs +++ b/examples/value.rs @@ -33,6 +33,6 @@ impl Options for Settings { } fn main() { - let color = Settings::default().parse(std::env::args_os()).color; - println!("{:?}", color); + let (settings, _operands) = Settings::default().parse(std::env::args_os()); + println!("{:?}", settings.color); } diff --git a/src/lib.rs b/src/lib.rs index 342bf9b..5f4ecc5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,6 +21,7 @@ use std::{ffi::OsString, marker::PhantomData}; pub enum Argument { Help, Version, + Positional(OsString), Custom(T), } @@ -59,18 +60,7 @@ pub trait Arguments: Sized { /// Parse the next argument from the lexopt parser. /// /// This method is called by [`ArgumentIter::next_arg`]. - fn next_arg( - parser: &mut lexopt::Parser, - positional_idx: &mut usize, - ) -> Result>, ErrorKind>; - - /// Check for any required arguments that have not been found. - /// - /// If any missing arguments are found, the appropriate error is returned. - /// The `positional_idx` parameter specifies how many positional arguments - /// have been passed so far. This method is called at the end of - /// [`Options::parse`] and [`Options::try_parse`]. - fn check_missing(positional_idx: usize) -> Result<(), Error>; + fn next_arg(parser: &mut lexopt::Parser) -> Result>, ErrorKind>; /// Print the help string for this command. /// @@ -117,7 +107,7 @@ pub trait Arguments: Sized { /// [`Options::try_parse`]. pub struct ArgumentIter { parser: lexopt::Parser, - pub positional_idx: usize, + positional_arguments: Vec, t: PhantomData, } @@ -129,18 +119,16 @@ impl ArgumentIter { { Self { parser: lexopt::Parser::from_iter(args), - positional_idx: 0, + positional_arguments: Vec::new(), t: PhantomData, } } pub fn next_arg(&mut self) -> Result, Error> { - if let Some(arg) = - T::next_arg(&mut self.parser, &mut self.positional_idx).map_err(|kind| Error { - exit_code: T::EXIT_CODE, - kind, - })? - { + while let Some(arg) = T::next_arg(&mut self.parser).map_err(|kind| Error { + exit_code: T::EXIT_CODE, + kind, + })? { match arg { Argument::Help => { self.help().unwrap(); @@ -150,11 +138,17 @@ impl ArgumentIter { print!("{}", self.version()); std::process::exit(0); } - Argument::Custom(arg) => Ok(Some(arg)), + Argument::Positional(arg) => { + self.positional_arguments.push(arg); + } + Argument::Custom(arg) => return Ok(Some(arg)), } - } else { - Ok(None) } + Ok(None) + } + + fn get_positional_arguments(self) -> Vec { + self.positional_arguments } fn help(&self) -> std::io::Result<()> { @@ -176,15 +170,13 @@ impl ArgumentIter { /// /// By default, the [`Options::parse`] method will /// 1. repeatedly call [`ArgumentIter::next_arg`] and call [`Options::apply`] -/// on the result until the arguments are exhausted, -/// 2. and finally call [`Arguments::check_missing`] to check whether all -/// required arguments were given. +/// on the result until the arguments are exhausted. pub trait Options: Sized { /// Apply a single argument to the options. fn apply(&mut self, arg: Arg); /// Parse an iterator of arguments into the options - fn parse(self, args: I) -> Self + fn parse(self, args: I) -> (Self, Vec) where I: IntoIterator, I::Item: Into, @@ -193,7 +185,7 @@ pub trait Options: Sized { } #[allow(unused_mut)] - fn try_parse(mut self, args: I) -> Result + fn try_parse(mut self, args: I) -> Result<(Self, Vec), Error> where I: IntoIterator, I::Item: Into, @@ -216,8 +208,7 @@ pub trait Options: Sized { while let Some(arg) = iter.next_arg()? { self.apply(arg); } - Arg::check_missing(iter.positional_idx)?; - Ok(self) + Ok((self, iter.get_positional_arguments())) } } diff --git a/tests/coreutils/arch.rs b/tests/coreutils/arch.rs index d3198d3..e69b639 100644 --- a/tests/coreutils/arch.rs +++ b/tests/coreutils/arch.rs @@ -12,5 +12,4 @@ fn no_args() { fn one_arg_fails() { assert!(Arg::try_check(["arch", "-f"]).is_err()); assert!(Arg::try_check(["arch", "--foo"]).is_err()); - assert!(Arg::try_check(["arch", "foo"]).is_err()); } diff --git a/tests/coreutils/b2sum.rs b/tests/coreutils/b2sum.rs index 0fcfc81..adae103 100644 --- a/tests/coreutils/b2sum.rs +++ b/tests/coreutils/b2sum.rs @@ -1,4 +1,4 @@ -use std::path::{Path, PathBuf}; +use std::ffi::OsString; use uutils_args::{Arguments, Options}; #[derive(Clone, Arguments)] @@ -26,9 +26,6 @@ enum Arg { #[arg("-w", "--warn")] Warn, - - #[arg("FILE", ..)] - File(PathBuf), } #[derive(Default, Debug, PartialEq, Eq)] @@ -46,7 +43,6 @@ struct Settings { tag: bool, check_output: CheckOutput, strict: bool, - files: Vec, } impl Options for Settings { @@ -60,58 +56,68 @@ impl Options for Settings { Arg::Status => self.check_output = CheckOutput::Status, Arg::Strict => self.strict = true, Arg::Warn => self.check_output = CheckOutput::Warn, - Arg::File(f) => self.files.push(f), } } } #[test] fn binary() { - assert!(!Settings::default().parse(["b2sum"]).binary); - assert!(!Settings::default().parse(["b2sum", "--text"]).binary); - assert!(!Settings::default().parse(["b2sum", "-t"]).binary); + assert!(!Settings::default().parse(["b2sum"]).0.binary); + assert!(!Settings::default().parse(["b2sum", "--text"]).0.binary); + assert!(!Settings::default().parse(["b2sum", "-t"]).0.binary); assert!( !Settings::default() .parse(["b2sum", "--binary", "--text"]) + .0 .binary ); - assert!(!Settings::default().parse(["b2sum", "-b", "-t"]).binary); + assert!(!Settings::default().parse(["b2sum", "-b", "-t"]).0.binary); - assert!(Settings::default().parse(["b2sum", "--binary"]).binary); - assert!(Settings::default().parse(["b2sum", "-b"]).binary); + assert!(Settings::default().parse(["b2sum", "--binary"]).0.binary); + assert!(Settings::default().parse(["b2sum", "-b"]).0.binary); assert!( Settings::default() .parse(["b2sum", "--text", "--binary"]) + .0 .binary ); - assert!(Settings::default().parse(["b2sum", "-t", "-b"]).binary); + assert!(Settings::default().parse(["b2sum", "-t", "-b"]).0.binary); } #[test] fn check_output() { assert_eq!( - Settings::default().parse(["b2sum", "--warn"]).check_output, + Settings::default() + .parse(["b2sum", "--warn"]) + .0 + .check_output, CheckOutput::Warn ); assert_eq!( - Settings::default().parse(["b2sum", "--quiet"]).check_output, + Settings::default() + .parse(["b2sum", "--quiet"]) + .0 + .check_output, CheckOutput::Quiet ); assert_eq!( Settings::default() .parse(["b2sum", "--status"]) + .0 .check_output, CheckOutput::Status ); assert_eq!( Settings::default() .parse(["b2sum", "--status", "--warn"]) + .0 .check_output, CheckOutput::Warn ); assert_eq!( Settings::default() .parse(["b2sum", "--status", "--warn"]) + .0 .check_output, CheckOutput::Warn ); @@ -119,6 +125,7 @@ fn check_output() { assert_eq!( Settings::default() .parse(["b2sum", "--warn", "--quiet"]) + .0 .check_output, CheckOutput::Quiet ); @@ -126,6 +133,7 @@ fn check_output() { assert_eq!( Settings::default() .parse(["b2sum", "--quiet", "--status"]) + .0 .check_output, CheckOutput::Status ); @@ -134,7 +142,7 @@ fn check_output() { #[test] fn files() { assert_eq!( - Settings::default().parse(["b2sum", "foo", "bar"]).files, - vec![Path::new("foo"), Path::new("bar")] + Settings::default().parse(["b2sum", "foo", "bar"]).1, + vec![OsString::from("foo"), OsString::from("bar")] ); } diff --git a/tests/coreutils/base32.rs b/tests/coreutils/base32.rs index 3ec67ad..be54c57 100644 --- a/tests/coreutils/base32.rs +++ b/tests/coreutils/base32.rs @@ -1,5 +1,3 @@ -use std::path::PathBuf; - use uutils_args::{Arguments, Options}; #[derive(Clone, Arguments)] @@ -12,16 +10,12 @@ enum Arg { #[arg("-w COLS", "--wrap=COLS")] Wrap(usize), - - #[arg("FILE", ..=1)] - File(PathBuf), } struct Settings { decode: bool, ignore_garbage: bool, wrap: Option, - file: Option, } impl Default for Settings { @@ -30,7 +24,6 @@ impl Default for Settings { wrap: Some(76), decode: Default::default(), ignore_garbage: Default::default(), - file: Default::default(), } } } @@ -42,21 +35,20 @@ impl Options for Settings { Arg::IgnoreGarbage => self.ignore_garbage = true, Arg::Wrap(0) => self.wrap = None, Arg::Wrap(x) => self.wrap = Some(x), - Arg::File(f) => self.file = Some(f), } } } #[test] fn wrap() { - assert_eq!(Settings::default().parse(["base32"]).wrap, Some(76)); - assert_eq!(Settings::default().parse(["base32", "-w0"]).wrap, None); + assert_eq!(Settings::default().parse(["base32"]).0.wrap, Some(76)); + assert_eq!(Settings::default().parse(["base32", "-w0"]).0.wrap, None); assert_eq!( - Settings::default().parse(["base32", "-w100"]).wrap, + Settings::default().parse(["base32", "-w100"]).0.wrap, Some(100) ); assert_eq!( - Settings::default().parse(["base32", "--wrap=100"]).wrap, + Settings::default().parse(["base32", "--wrap=100"]).0.wrap, Some(100) ); } diff --git a/tests/coreutils/basename.rs b/tests/coreutils/basename.rs index 66d598a..accb8ce 100644 --- a/tests/coreutils/basename.rs +++ b/tests/coreutils/basename.rs @@ -1,3 +1,5 @@ +use std::ffi::OsString; + use uutils_args::{Arguments, Options}; #[derive(Clone, Arguments)] @@ -6,21 +8,18 @@ enum Arg { Multiple, #[arg("-s SUFFIX", "--suffix=SUFFIX")] - Suffix(String), + Suffix(OsString), #[arg("-z", "--zero")] Zero, - - #[arg("NAMES", last, ..)] - Names(Vec), } #[derive(Default)] struct Settings { multiple: bool, - suffix: String, + suffix: OsString, zero: bool, - names: Vec, + names: Vec, } impl Options for Settings { @@ -32,13 +31,13 @@ impl Options for Settings { self.suffix = s } Arg::Zero => self.zero = true, - Arg::Names(names) => self.names = names, } } } fn parse(args: &[&str]) -> Settings { - let mut settings = Settings::default().parse(args); + let (mut settings, operands) = Settings::default().parse(args); + settings.names = operands; if !settings.multiple { assert_eq!(settings.names.len(), 2); settings.suffix = settings.names.pop().unwrap(); diff --git a/tests/coreutils/cat.rs b/tests/coreutils/cat.rs index b809b91..190d6c1 100644 --- a/tests/coreutils/cat.rs +++ b/tests/coreutils/cat.rs @@ -1,5 +1,3 @@ -use std::path::PathBuf; - use uutils_args::{Arguments, Options}; #[derive(Default)] @@ -38,9 +36,6 @@ enum Arg { #[arg("-v", "--show-nonprinting")] ShowNonPrinting, - - #[arg("FILES", ..)] - File(PathBuf), } #[derive(Default)] @@ -50,7 +45,6 @@ struct Settings { show_nonprinting: bool, number: NumberingMode, squeeze_blank: bool, - files: Vec, } impl Options for Settings { @@ -75,34 +69,33 @@ impl Options for Settings { Arg::Number => self.number = NumberingMode::All, Arg::NumberNonblank => self.number = NumberingMode::NonEmpty, Arg::SqueezeBlank => self.squeeze_blank = true, - Arg::File(f) => self.files.push(f), } } } #[test] fn show() { - let s = Settings::default().parse(["cat", "-v"]); + let (s, _) = Settings::default().parse(["cat", "-v"]); assert!(!s.show_ends && !s.show_tabs && s.show_nonprinting); - let s = Settings::default().parse(["cat", "-E"]); + let (s, _) = Settings::default().parse(["cat", "-E"]); assert!(s.show_ends && !s.show_tabs && !s.show_nonprinting); - let s = Settings::default().parse(["cat", "-T"]); + let (s, _) = Settings::default().parse(["cat", "-T"]); assert!(!s.show_ends && s.show_tabs && !s.show_nonprinting); - let s = Settings::default().parse(["cat", "-e"]); + let (s, _) = Settings::default().parse(["cat", "-e"]); assert!(s.show_ends && !s.show_tabs && s.show_nonprinting); - let s = Settings::default().parse(["cat", "-t"]); + let (s, _) = Settings::default().parse(["cat", "-t"]); assert!(!s.show_ends && s.show_tabs && s.show_nonprinting); - let s = Settings::default().parse(["cat", "-A"]); + let (s, _) = Settings::default().parse(["cat", "-A"]); assert!(s.show_ends && s.show_tabs && s.show_nonprinting); - let s = Settings::default().parse(["cat", "-te"]); + let (s, _) = Settings::default().parse(["cat", "-te"]); assert!(s.show_ends && s.show_tabs && s.show_nonprinting); - let s = Settings::default().parse(["cat", "-vET"]); + let (s, _) = Settings::default().parse(["cat", "-vET"]); assert!(s.show_ends && s.show_tabs && s.show_nonprinting); } diff --git a/tests/coreutils/dd.rs b/tests/coreutils/dd.rs index 6d9c28a..20ae14c 100644 --- a/tests/coreutils/dd.rs +++ b/tests/coreutils/dd.rs @@ -116,16 +116,13 @@ impl Options for Settings { #[test] fn empty() { - assert_eq!( - Settings::default().try_parse(["dd"]).unwrap(), - Settings::default() - ) + assert_eq!(Settings::default().parse(["dd"]).0, Settings::default()) } #[test] fn infile() { assert_eq!( - Settings::default().try_parse(["dd", "if=hello"]).unwrap(), + Settings::default().parse(["dd", "if=hello"]).0, Settings { infile: Some(PathBuf::from("hello")), ..Settings::default() @@ -136,7 +133,7 @@ fn infile() { #[test] fn outfile() { assert_eq!( - Settings::default().try_parse(["dd", "of=hello"]).unwrap(), + Settings::default().parse(["dd", "of=hello"]).0, Settings { outfile: Some(PathBuf::from("hello")), ..Settings::default() @@ -147,7 +144,7 @@ fn outfile() { #[test] fn bs() { assert_eq!( - Settings::default().try_parse(["dd", "ibs=1"]).unwrap(), + Settings::default().parse(["dd", "ibs=1"]).0, Settings { ibs: 1, obs: 512, @@ -155,7 +152,7 @@ fn bs() { } ); assert_eq!( - Settings::default().try_parse(["dd", "obs=1"]).unwrap(), + Settings::default().parse(["dd", "obs=1"]).0, Settings { ibs: 512, obs: 1, @@ -163,9 +160,7 @@ fn bs() { } ); assert_eq!( - Settings::default() - .try_parse(["dd", "ibs=10", "obs=1"]) - .unwrap(), + Settings::default().parse(["dd", "ibs=10", "obs=1"]).0, Settings { ibs: 10, obs: 1, @@ -173,9 +168,7 @@ fn bs() { } ); assert_eq!( - Settings::default() - .try_parse(["dd", "ibs=10", "bs=1"]) - .unwrap(), + Settings::default().parse(["dd", "ibs=10", "bs=1"]).0, Settings { ibs: 1, obs: 1, diff --git a/tests/coreutils/echo.rs b/tests/coreutils/echo.rs index 69f482e..4016c46 100644 --- a/tests/coreutils/echo.rs +++ b/tests/coreutils/echo.rs @@ -15,16 +15,12 @@ enum Arg { /// Disable interpretation of backslash escapes #[arg("-E")] DisableEscape, - - #[arg("STRING", last)] - String(Vec), } #[derive(Default)] struct Settings { trailing_newline: bool, escape: bool, - strings: Vec, } impl Options for Settings { @@ -33,7 +29,6 @@ impl Options for Settings { Arg::NoNewline => self.trailing_newline = false, Arg::EnableEscape => self.escape = true, Arg::DisableEscape => self.escape = false, - Arg::String(s) => self.strings = s, } } } @@ -42,17 +37,18 @@ impl Options for Settings { // support explicitly. #[test] +#[ignore = "needs to be fixed after positional argument refactor"] fn double_hyphen() { - let s = Settings::default().parse(["echo", "--"]); - assert_eq!(s.strings, vec![OsString::from("--")]); + let (_, operands) = Settings::default().parse(["echo", "--"]); + assert_eq!(operands, vec![OsString::from("--")]); - let s = Settings::default().parse(["echo", "--", "-n"]); - assert_eq!(s.strings, vec![OsString::from("--"), OsString::from("-n")]); + let (_, operands) = Settings::default().parse(["echo", "--", "-n"]); + assert_eq!(operands, vec![OsString::from("--"), OsString::from("-n")]); } #[test] #[ignore] fn nonexistent_options_are_values() { - let s = Settings::default().parse(["echo", "-f"]); - assert_eq!(s.strings, vec![OsString::from("-f")]); + let (_, operands) = Settings::default().parse(["echo", "-f"]); + assert_eq!(operands, vec![OsString::from("-f")]); } diff --git a/tests/coreutils/head.rs b/tests/coreutils/head.rs index c1257c7..ab08a9e 100644 --- a/tests/coreutils/head.rs +++ b/tests/coreutils/head.rs @@ -8,7 +8,7 @@ use uutils_args::{Arguments, Options, Value}; // from this function are not relevant, so we can just return an `Option`. // Once this gets into uutils, I highly recommend that we make this format // optional at compile time. As the GNU docs explain, it's very error-prone. -fn parse_deprecated(iter: I) -> Option +fn parse_deprecated(iter: I) -> Option<(Settings, Vec)> where I: IntoIterator + Clone, I::Item: Into, @@ -73,13 +73,15 @@ where } } - Some(Settings { - number: SigNum::Negative(num), - mode, - inputs: vec![input.into().into()], - verbose, - zero, - }) + Some(( + Settings { + number: SigNum::Negative(num), + mode, + verbose, + zero, + }, + vec![input.into()], + )) } #[derive(Arguments)] @@ -98,9 +100,6 @@ enum Arg { #[arg("-z", "--zero-terminated")] Zero, - - #[arg("FILES", ..)] - File(PathBuf), } // We need both negative and positive 0 @@ -185,7 +184,6 @@ struct Settings { number: SigNum, // TODO: Should be a dedicated PID type verbose: bool, - inputs: Vec, zero: bool, } @@ -203,12 +201,11 @@ impl Options for Settings { Arg::Quiet => self.verbose = false, Arg::Verbose => self.verbose = true, Arg::Zero => self.zero = true, - Arg::File(input) => self.inputs.push(input), } } } -fn parse_head(iter: I) -> Result +fn parse_head(iter: I) -> Result<(Settings, Vec), uutils_args::Error> where I: IntoIterator + Clone, I::Item: Into, @@ -221,51 +218,51 @@ where #[test] fn shorthand() { - let s = parse_head(["head", "-20", "some_file"]).unwrap(); + let (s, _operands) = parse_head(["head", "-20", "some_file"]).unwrap(); assert_eq!(s.number, SigNum::Negative(20)); assert_eq!(s.mode, Mode::Lines); - let s = parse_head(["head", "-100cq", "some_file"]).unwrap(); + let (s, _operands) = parse_head(["head", "-100cq", "some_file"]).unwrap(); assert_eq!(s.number, SigNum::Negative(100)); assert_eq!(s.mode, Mode::Bytes); // Corner case where the shorthand does not apply - let s = parse_head(["head", "-c", "42"]).unwrap(); + let (s, operands) = parse_head(["head", "-c", "42"]).unwrap(); assert_eq!(s.number, SigNum::Negative(42)); assert_eq!(s.mode, Mode::Bytes); - assert_eq!(s.inputs, Vec::::new()); + assert_eq!(operands, Vec::::new()); } #[test] fn standard_input() { - let s = parse_head(["head", "-"]).unwrap(); - assert_eq!(s.inputs, vec![PathBuf::from("-")]) + let (_s, operands) = parse_head(["head", "-"]).unwrap(); + assert_eq!(operands, vec![PathBuf::from("-")]) } #[test] fn normal_format() { - let s = parse_head(["head", "-c", "20", "some_file"]).unwrap(); + let (s, _operands) = parse_head(["head", "-c", "20", "some_file"]).unwrap(); assert_eq!(s.number, SigNum::Negative(20)); assert_eq!(s.mode, Mode::Bytes); } #[test] fn signum() { - let s = parse_head(["head", "-n", "20"]).unwrap(); + let (s, _operands) = parse_head(["head", "-n", "20"]).unwrap(); assert_eq!(s.number, SigNum::Negative(20)); - let s = parse_head(["head", "-n", "-20"]).unwrap(); + let (s, _operands) = parse_head(["head", "-n", "-20"]).unwrap(); assert_eq!(s.number, SigNum::Negative(20)); - let s = parse_head(["head", "-n", "+20"]).unwrap(); + let (s, _operands) = parse_head(["head", "-n", "+20"]).unwrap(); assert_eq!(s.number, SigNum::Positive(20)); - let s = parse_head(["head", "-n", "20b"]).unwrap(); + let (s, _operands) = parse_head(["head", "-n", "20b"]).unwrap(); assert_eq!(s.number, SigNum::Negative(20 * 512)); - let s = parse_head(["head", "-n", "+20b"]).unwrap(); + let (s, _operands) = parse_head(["head", "-n", "+20b"]).unwrap(); assert_eq!(s.number, SigNum::Positive(20 * 512)); - let s = parse_head(["head", "-n", "b"]).unwrap(); + let (s, _operands) = parse_head(["head", "-n", "b"]).unwrap(); assert_eq!(s.number, SigNum::Negative(512)); - let s = parse_head(["head", "-n", "+b"]).unwrap(); + let (s, _operands) = parse_head(["head", "-n", "+b"]).unwrap(); assert_eq!(s.number, SigNum::Positive(512)); assert!(parse_head(["head", "-n", "20invalid_suffix"]).is_err()); diff --git a/tests/coreutils/ls.rs b/tests/coreutils/ls.rs index 1ebcfa4..1a23c92 100644 --- a/tests/coreutils/ls.rs +++ b/tests/coreutils/ls.rs @@ -1,4 +1,3 @@ -use std::path::PathBuf; use uutils_args::{Arguments, Options, Value}; #[derive(Default, Debug, PartialEq, Eq, Value)] @@ -276,9 +275,6 @@ enum Arg { #[arg("--group-directories-first")] GroupDirectoriesFirst, - - #[arg("FILES", ..)] - File(PathBuf), } fn default_terminal_size() -> u16 { @@ -305,7 +301,6 @@ fn default_terminal_size() -> u16 { #[derive(Debug, PartialEq, Eq)] struct Settings { format: Format, - files: Vec, sort: Sort, recursive: bool, reverse: bool, @@ -340,7 +335,6 @@ impl Default for Settings { eol: '\n', width: default_terminal_size(), format: Default::default(), - files: Default::default(), sort: Default::default(), recursive: Default::default(), reverse: Default::default(), @@ -422,7 +416,6 @@ impl Options for Settings { // TODO: Zero changes more than just this } Arg::GroupDirectoriesFirst => self.group_directories_first = true, - Arg::File(f) => self.files.push(f), } } } @@ -430,10 +423,9 @@ impl Options for Settings { #[test] fn default() { assert_eq!( - Settings::default().parse(["ls"]), + Settings::default().parse(["ls"]).0, Settings { format: Format::Columns, - files: Vec::new(), sort: Sort::Name, recursive: false, reverse: false, @@ -462,87 +454,87 @@ fn default() { #[test] fn color() { - let s = Settings::default().parse(["ls", "--color"]); + let (s, _operands) = Settings::default().parse(["ls", "--color"]); assert!(s.color); - let s = Settings::default().parse(["ls", "--color=always"]); + let (s, _operands) = Settings::default().parse(["ls", "--color=always"]); assert!(s.color); - let s = Settings::default().parse(["ls", "--color=never"]); + let (s, _operands) = Settings::default().parse(["ls", "--color=never"]); assert!(!s.color); } #[test] fn format() { - let s = Settings::default().parse(["ls", "-l"]); + let (s, _operands) = Settings::default().parse(["ls", "-l"]); assert_eq!(s.format, Format::Long); - let s = Settings::default().parse(["ls", "-m"]); + let (s, _operands) = Settings::default().parse(["ls", "-m"]); assert_eq!(s.format, Format::Commas); - let s = Settings::default().parse(["ls", "--format=across"]); + let (s, _operands) = Settings::default().parse(["ls", "--format=across"]); assert_eq!(s.format, Format::Across); - let s = Settings::default().parse(["ls", "--format=acr"]); + let (s, _operands) = Settings::default().parse(["ls", "--format=acr"]); assert_eq!(s.format, Format::Across); - let s = Settings::default().parse(["ls", "-o"]); + let (s, _operands) = Settings::default().parse(["ls", "-o"]); assert_eq!(s.format, Format::Long); assert!(s.long_no_group && !s.long_no_owner && !s.long_numeric_uid_gid); - let s = Settings::default().parse(["ls", "-g"]); + let (s, _operands) = Settings::default().parse(["ls", "-g"]); assert_eq!(s.format, Format::Long); assert!(!s.long_no_group && s.long_no_owner && !s.long_numeric_uid_gid); - let s = Settings::default().parse(["ls", "-n"]); + let (s, _operands) = Settings::default().parse(["ls", "-n"]); assert_eq!(s.format, Format::Long); assert!(!s.long_no_group && !s.long_no_owner && s.long_numeric_uid_gid); - let s = Settings::default().parse(["ls", "-og"]); + let (s, _operands) = Settings::default().parse(["ls", "-og"]); assert_eq!(s.format, Format::Long); assert!(s.long_no_group && s.long_no_owner && !s.long_numeric_uid_gid); - let s = Settings::default().parse(["ls", "-on"]); + let (s, _operands) = Settings::default().parse(["ls", "-on"]); assert_eq!(s.format, Format::Long); assert!(s.long_no_group && !s.long_no_owner && s.long_numeric_uid_gid); - let s = Settings::default().parse(["ls", "-onCl"]); + let (s, _operands) = Settings::default().parse(["ls", "-onCl"]); assert_eq!(s.format, Format::Long); assert!(s.long_no_group && !s.long_no_owner && s.long_numeric_uid_gid); } #[test] fn time() { - let s = Settings::default().parse(["ls", "--time=access"]); + let (s, _operands) = Settings::default().parse(["ls", "--time=access"]); assert_eq!(s.time, Time::Access); - let s = Settings::default().parse(["ls", "--time=a"]); + let (s, _operands) = Settings::default().parse(["ls", "--time=a"]); assert_eq!(s.time, Time::Access); } #[test] fn classify() { - let s = Settings::default().parse(["ls", "--indicator-style=classify"]); + let (s, _operands) = Settings::default().parse(["ls", "--indicator-style=classify"]); assert_eq!(s.indicator_style, IndicatorStyle::Classify); - let s = Settings::default().parse(["ls", "--classify"]); + let (s, _operands) = Settings::default().parse(["ls", "--classify"]); assert_eq!(s.indicator_style, IndicatorStyle::Classify); - let s = Settings::default().parse(["ls", "--classify=always"]); + let (s, _operands) = Settings::default().parse(["ls", "--classify=always"]); assert_eq!(s.indicator_style, IndicatorStyle::Classify); - let s = Settings::default().parse(["ls", "--classify=none"]); + let (s, _operands) = Settings::default().parse(["ls", "--classify=none"]); assert_eq!(s.indicator_style, IndicatorStyle::None); - let s = Settings::default().parse(["ls", "-F"]); + let (s, _operands) = Settings::default().parse(["ls", "-F"]); assert_eq!(s.indicator_style, IndicatorStyle::Classify); } #[test] fn sort() { - let s = Settings::default().parse(["ls", "--sort=time"]); + let (s, _operands) = Settings::default().parse(["ls", "--sort=time"]); assert_eq!(s.sort, Sort::Time); - let s = Settings::default().parse(["ls", "-X"]); + let (s, _operands) = Settings::default().parse(["ls", "-X"]); assert_eq!(s.sort, Sort::Extension); } diff --git a/tests/coreutils/mktemp.rs b/tests/coreutils/mktemp.rs index 344cb06..c90d019 100644 --- a/tests/coreutils/mktemp.rs +++ b/tests/coreutils/mktemp.rs @@ -21,9 +21,6 @@ enum Arg { #[arg("-p DIR", "--tmpdir[=DIR]", value = ".".into())] TmpDir(PathBuf), - - #[arg("TEMPLATE", 0..=1)] - Template(String), } #[derive(Default)] @@ -34,7 +31,6 @@ struct Settings { tmp_dir: Option, suffix: Option, treat_as_template: bool, - template: String, } impl Options for Settings { @@ -46,41 +42,40 @@ impl Options for Settings { Arg::Suffix(s) => self.suffix = Some(s), Arg::TreatAsTemplate => self.treat_as_template = true, Arg::TmpDir(dir) => self.tmp_dir = Some(dir), - Arg::Template(s) => self.template = s, } } } #[test] fn suffix() { - let s = Settings::default().parse(["mktemp", "--suffix=hello"]); + let (s, _operands) = Settings::default().parse(["mktemp", "--suffix=hello"]); assert_eq!(s.suffix.unwrap(), "hello"); - let s = Settings::default().parse(["mktemp", "--suffix="]); + let (s, _operands) = Settings::default().parse(["mktemp", "--suffix="]); assert_eq!(s.suffix.unwrap(), ""); - let s = Settings::default().parse(["mktemp", "--suffix="]); + let (s, _operands) = Settings::default().parse(["mktemp", "--suffix="]); assert_eq!(s.suffix.unwrap(), ""); - let s = Settings::default().parse(["mktemp"]); + let (s, _operands) = Settings::default().parse(["mktemp"]); assert_eq!(s.suffix, None); } #[test] fn tmpdir() { - let s = Settings::default().parse(["mktemp", "--tmpdir"]); + let (s, _operands) = Settings::default().parse(["mktemp", "--tmpdir"]); assert_eq!(s.tmp_dir.unwrap(), Path::new(".")); - let s = Settings::default().parse(["mktemp", "--tmpdir="]); + let (s, _operands) = Settings::default().parse(["mktemp", "--tmpdir="]); assert_eq!(s.tmp_dir.unwrap(), Path::new("")); - let s = Settings::default().parse(["mktemp", "-p", "foo"]); + let (s, _operands) = Settings::default().parse(["mktemp", "-p", "foo"]); assert_eq!(s.tmp_dir.unwrap(), Path::new("foo")); - let s = Settings::default().parse(["mktemp", "-pfoo"]); + let (s, _operands) = Settings::default().parse(["mktemp", "-pfoo"]); assert_eq!(s.tmp_dir.unwrap(), Path::new("foo")); - let s = Settings::default().parse(["mktemp", "-p", ""]); + let (s, _operands) = Settings::default().parse(["mktemp", "-p", ""]); assert_eq!(s.tmp_dir.unwrap(), Path::new("")); assert!(Settings::default().try_parse(["mktemp", "-p"]).is_err()); diff --git a/tests/coreutils/tail.rs b/tests/coreutils/tail.rs index 7e6b63e..f8f75d2 100644 --- a/tests/coreutils/tail.rs +++ b/tests/coreutils/tail.rs @@ -8,7 +8,7 @@ use uutils_args::{Arguments, Options, Value}; // from this function are not relevant, so we can just return an `Option`. // Once this gets into uutils, I highly recommend that we make this format // optional at compile time. As the GNU docs explain, it's very error-prone. -fn parse_deprecated(iter: I) -> Option +fn parse_deprecated(iter: I) -> Option<(Settings, Vec)> where I: IntoIterator + Clone, I::Item: Into, @@ -95,13 +95,15 @@ where return None; } - Some(Settings { - number: sig(num), - mode, - follow, - inputs: vec![input.into().into()], - ..Settings::default() - }) + Some(( + Settings { + number: sig(num), + mode, + follow, + ..Settings::default() + }, + vec![input.into()], + )) } #[derive(Arguments)] @@ -141,9 +143,6 @@ enum Arg { #[arg("---presume-input-pipe", hidden)] PresumeInputPipe, - - #[arg("FILES", ..)] - File(PathBuf), } // We need both negative and positive 0 @@ -267,13 +266,12 @@ impl Options for Settings { Arg::SleepInterval(n) => self.sleep_sec = n, Arg::Verbose => self.verbose = true, Arg::Zero => self.zero = true, - Arg::File(input) => self.inputs.push(input), Arg::PresumeInputPipe => self.presume_input_pipe = true, } } } -fn parse_tail(iter: I) -> Result +fn parse_tail(iter: I) -> Result<(Settings, Vec), uutils_args::Error> where I: IntoIterator + Clone, I::Item: Into, @@ -286,23 +284,23 @@ where #[test] fn shorthand() { - let s = parse_tail(["tail", "-20", "some_file"]).unwrap(); + let (s, _operands) = parse_tail(["tail", "-20", "some_file"]).unwrap(); assert_eq!(s.number, SigNum::Negative(20)); assert_eq!(s.mode, Mode::Lines); assert_eq!(s.follow, None); - let s = parse_tail(["tail", "+20", "some_file"]).unwrap(); + let (s, _operands) = parse_tail(["tail", "+20", "some_file"]).unwrap(); assert_eq!(s.number, SigNum::Positive(20)); assert_eq!(s.mode, Mode::Lines); assert_eq!(s.follow, None); - let s = parse_tail(["tail", "-100cf", "some_file"]).unwrap(); + let (s, _operands) = parse_tail(["tail", "-100cf", "some_file"]).unwrap(); assert_eq!(s.number, SigNum::Negative(100)); assert_eq!(s.mode, Mode::Bytes); assert_eq!(s.follow, Some(FollowMode::Descriptor)); // Corner case where the shorthand does not apply - let s = parse_tail(["tail", "-c", "42"]).unwrap(); + let (s, _operands) = parse_tail(["tail", "-c", "42"]).unwrap(); assert_eq!(s.number, SigNum::Negative(42)); assert_eq!(s.mode, Mode::Bytes); assert_eq!(s.inputs, Vec::::new()); @@ -310,34 +308,34 @@ fn shorthand() { #[test] fn standard_input() { - let s = parse_tail(["tail", "-"]).unwrap(); - assert_eq!(s.inputs, vec![PathBuf::from("-")]) + let (_s, operands) = parse_tail(["tail", "-"]).unwrap(); + assert_eq!(operands, vec![PathBuf::from("-")]) } #[test] fn normal_format() { - let s = parse_tail(["tail", "-c", "20", "some_file"]).unwrap(); + let (s, _operands) = parse_tail(["tail", "-c", "20", "some_file"]).unwrap(); assert_eq!(s.number, SigNum::Negative(20)); assert_eq!(s.mode, Mode::Bytes); } #[test] fn signum() { - let s = parse_tail(["tail", "-n", "20"]).unwrap(); + let (s, _operands) = parse_tail(["tail", "-n", "20"]).unwrap(); assert_eq!(s.number, SigNum::Negative(20)); - let s = parse_tail(["tail", "-n", "-20"]).unwrap(); + let (s, _operands) = parse_tail(["tail", "-n", "-20"]).unwrap(); assert_eq!(s.number, SigNum::Negative(20)); - let s = parse_tail(["tail", "-n", "+20"]).unwrap(); + let (s, _operands) = parse_tail(["tail", "-n", "+20"]).unwrap(); assert_eq!(s.number, SigNum::Positive(20)); - let s = parse_tail(["tail", "-n", "20b"]).unwrap(); + let (s, _operands) = parse_tail(["tail", "-n", "20b"]).unwrap(); assert_eq!(s.number, SigNum::Negative(20 * 512)); - let s = parse_tail(["tail", "-n", "+20b"]).unwrap(); + let (s, _operands) = parse_tail(["tail", "-n", "+20b"]).unwrap(); assert_eq!(s.number, SigNum::Positive(20 * 512)); - let s = parse_tail(["tail", "-n", "b"]).unwrap(); + let (s, _operands) = parse_tail(["tail", "-n", "b"]).unwrap(); assert_eq!(s.number, SigNum::Negative(512)); - let s = parse_tail(["tail", "-n", "+b"]).unwrap(); + let (s, _operands) = parse_tail(["tail", "-n", "+b"]).unwrap(); assert_eq!(s.number, SigNum::Positive(512)); assert!(parse_tail(["tail", "-n", "20invalid_suffix"]).is_err()); @@ -346,35 +344,35 @@ fn signum() { #[test] fn follow_mode() { // Sanity check: should be None initially - let s = parse_tail(["tail"]).unwrap(); + let (s, _operands) = parse_tail(["tail"]).unwrap(); assert_eq!(s.follow, None); - let s = parse_tail(["tail", "--follow"]).unwrap(); + let (s, _operands) = parse_tail(["tail", "--follow"]).unwrap(); assert_eq!(s.follow, Some(FollowMode::Descriptor)); - let s = parse_tail(["tail", "-f"]).unwrap(); + let (s, _operands) = parse_tail(["tail", "-f"]).unwrap(); assert_eq!(s.follow, Some(FollowMode::Descriptor)); - let s = parse_tail(["tail", "--follow=descriptor"]).unwrap(); + let (s, _operands) = parse_tail(["tail", "--follow=descriptor"]).unwrap(); assert_eq!(s.follow, Some(FollowMode::Descriptor)); - let s = parse_tail(["tail", "--follow=des"]).unwrap(); + let (s, _operands) = parse_tail(["tail", "--follow=des"]).unwrap(); assert_eq!(s.follow, Some(FollowMode::Descriptor)); - let s = parse_tail(["tail", "--follow=d"]).unwrap(); + let (s, _operands) = parse_tail(["tail", "--follow=d"]).unwrap(); assert_eq!(s.follow, Some(FollowMode::Descriptor)); - let s = parse_tail(["tail", "--follow=name"]).unwrap(); + let (s, _operands) = parse_tail(["tail", "--follow=name"]).unwrap(); assert_eq!(s.follow, Some(FollowMode::Name)); - let s = parse_tail(["tail", "--follow=na"]).unwrap(); + let (s, _operands) = parse_tail(["tail", "--follow=na"]).unwrap(); assert_eq!(s.follow, Some(FollowMode::Name)); - let s = parse_tail(["tail", "--follow=n"]).unwrap(); + let (s, _operands) = parse_tail(["tail", "--follow=n"]).unwrap(); assert_eq!(s.follow, Some(FollowMode::Name)); assert!(parse_tail(["tail", "--follow="]).is_err()); - let s = parse_tail(["tail", "-F"]).unwrap(); + let (s, _operands) = parse_tail(["tail", "-F"]).unwrap(); assert_eq!(s.follow, Some(FollowMode::Name)); } diff --git a/tests/coreutils/uniq.rs b/tests/coreutils/uniq.rs index b8eb7e4..28a3264 100644 --- a/tests/coreutils/uniq.rs +++ b/tests/coreutils/uniq.rs @@ -5,10 +5,10 @@ use uutils_args::{Arguments, Initial, Options, Value}; enum Arg { #[option("-f N", "--skip-fields=n")] SkipFields(usize), - + #[option("-s N", "--skip-chars=N")] SkipChars(usize), - + #[option("-c", "--count")] Count, @@ -69,16 +69,16 @@ impl Options for Settings { match arg { Arg::SkipFields(n) => { self.skip_fields = Some(n); - }, + } Arg::SkipChars(n) => { self.slice_start = Some(n); - }, + } Arg::Count => { self.show_counts = true; - }, + } Arg::IgnoreCase => { self.ignore_case = true; - }, + } Arg::Repeated => { self.repeats_only = true; } @@ -86,20 +86,20 @@ impl Options for Settings { self.repeats_only = true; self.all_repeated = true; self.delimiters = d; - }, + } Arg::Group(d) => { self.all_repeated = true; self.delimiters = d; - }, + } Arg::Unique => { self.uniques_only = true; - }, + } Arg::CheckChars(n) => { self.slice_stop = Some(n); } Arg::ZeroTerminated => { self.zero_terminated = true; - }, + } } } -} \ No newline at end of file +} diff --git a/tests/flags.rs b/tests/flags.rs index 1c4bd64..c1960cf 100644 --- a/tests/flags.rs +++ b/tests/flags.rs @@ -21,7 +21,7 @@ fn one_flag() { } } - let settings = Settings::default().parse(["test", "-f"]); + let (settings, _) = Settings::default().parse(["test", "-f"]); assert!(settings.foo); } @@ -51,19 +51,19 @@ fn two_flags() { } assert_eq!( - Settings::default().parse(["test", "-a"]), + Settings::default().parse(["test", "-a"]).0, Settings { a: true, b: false } ); assert_eq!( - Settings::default().parse(["test"]), + Settings::default().parse(["test"]).0, Settings { a: false, b: false } ); assert_eq!( - Settings::default().parse(["test", "-b"]), + Settings::default().parse(["test", "-b"]).0, Settings { a: false, b: true } ); assert_eq!( - Settings::default().parse(["test", "-a", "-b"]), + Settings::default().parse(["test", "-a", "-b"]).0, Settings { a: true, b: true } ); } @@ -87,9 +87,9 @@ fn long_and_short_flag() { } } - assert!(!Settings::default().parse(["test"]).foo); - assert!(Settings::default().parse(["test", "--foo"]).foo); - assert!(Settings::default().parse(["test", "-f"]).foo); + assert!(!Settings::default().parse(["test"]).0.foo); + assert!(Settings::default().parse(["test", "--foo"]).0.foo); + assert!(Settings::default().parse(["test", "-f"]).0.foo); } #[test] @@ -111,7 +111,7 @@ fn short_alias() { } } - assert!(Settings::default().parse(["test", "-b"]).foo); + assert!(Settings::default().parse(["test", "-b"]).0.foo); } #[test] @@ -133,7 +133,7 @@ fn long_alias() { } } - assert!(Settings::default().parse(["test", "--bar"]).foo); + assert!(Settings::default().parse(["test", "--bar"]).0.foo); } #[test] @@ -171,10 +171,10 @@ fn short_and_long_alias() { bar: true, }; - assert_eq!(Settings::default().parse(["test", "--bar"]), foo_true); - assert_eq!(Settings::default().parse(["test", "-b"]), foo_true); - assert_eq!(Settings::default().parse(["test", "--foo"]), bar_true); - assert_eq!(Settings::default().parse(["test", "-f"]), bar_true); + assert_eq!(Settings::default().parse(["test", "--bar"]).0, foo_true); + assert_eq!(Settings::default().parse(["test", "-b"]).0, foo_true); + assert_eq!(Settings::default().parse(["test", "--foo"]).0, bar_true); + assert_eq!(Settings::default().parse(["test", "-f"]).0, bar_true); } #[test] @@ -217,7 +217,7 @@ fn xyz_map_to_abc() { } assert_eq!( - Settings::default().parse(["test", "-x"]), + Settings::default().parse(["test", "-x"]).0, Settings { a: true, b: true, @@ -226,7 +226,7 @@ fn xyz_map_to_abc() { ); assert_eq!( - Settings::default().parse(["test", "-y"]), + Settings::default().parse(["test", "-y"]).0, Settings { a: false, b: true, @@ -235,7 +235,7 @@ fn xyz_map_to_abc() { ); assert_eq!( - Settings::default().parse(["test", "-xy"]), + Settings::default().parse(["test", "-xy"]).0, Settings { a: true, b: true, @@ -244,7 +244,7 @@ fn xyz_map_to_abc() { ); assert_eq!( - Settings::default().parse(["test", "-z"]), + Settings::default().parse(["test", "-z"]).0, Settings { a: true, b: true, @@ -279,7 +279,9 @@ fn non_rust_ident() { } assert_eq!( - Settings::default().parse(["test", "--foo-bar", "--super"]), + Settings::default() + .parse(["test", "--foo-bar", "--super"]) + .0, Settings { a: true, b: true } ) } @@ -302,7 +304,7 @@ fn number_flag() { } } - assert!(Settings::default().parse(["test", "-1"]).one) + assert!(Settings::default().parse(["test", "-1"]).0.one) } #[test] @@ -329,12 +331,12 @@ fn false_bool() { } } - assert!(Settings::default().parse(["test", "-a"]).foo); - assert!(!Settings::default().parse(["test", "-b"]).foo); - assert!(!Settings::default().parse(["test", "-ab"]).foo); - assert!(Settings::default().parse(["test", "-ba"]).foo); - assert!(!Settings::default().parse(["test", "-a", "-b"]).foo); - assert!(Settings::default().parse(["test", "-b", "-a"]).foo); + assert!(Settings::default().parse(["test", "-a"]).0.foo); + assert!(!Settings::default().parse(["test", "-b"]).0.foo); + assert!(!Settings::default().parse(["test", "-ab"]).0.foo); + assert!(Settings::default().parse(["test", "-ba"]).0.foo); + assert!(!Settings::default().parse(["test", "-a", "-b"]).0.foo); + assert!(Settings::default().parse(["test", "-b", "-a"]).0.foo); } #[test] @@ -356,9 +358,9 @@ fn verbosity() { } } - assert_eq!(Settings::default().parse(["test", "-v"]).verbosity, 1); - assert_eq!(Settings::default().parse(["test", "-vv"]).verbosity, 2); - assert_eq!(Settings::default().parse(["test", "-vvv"]).verbosity, 3); + assert_eq!(Settings::default().parse(["test", "-v"]).0.verbosity, 1); + assert_eq!(Settings::default().parse(["test", "-vv"]).0.verbosity, 2); + assert_eq!(Settings::default().parse(["test", "-vvv"]).0.verbosity, 3); } #[test] @@ -390,9 +392,9 @@ fn infer_long_args() { } } - assert!(Settings::default().parse(["test", "--all"]).all); - assert!(Settings::default().parse(["test", "--alm"]).almost_all); - assert!(Settings::default().parse(["test", "--au"]).author); + assert!(Settings::default().parse(["test", "--all"]).0.all); + assert!(Settings::default().parse(["test", "--alm"]).0.almost_all); + assert!(Settings::default().parse(["test", "--au"]).0.author); assert!(Settings::default().try_parse(["test", "--a"]).is_err()); } @@ -431,13 +433,13 @@ fn enum_flag() { } } - assert_eq!(Settings::default().parse(["test"]).foo, SomeEnum::Foo); + assert_eq!(Settings::default().parse(["test"]).0.foo, SomeEnum::Foo); assert_eq!( - Settings::default().parse(["test", "--bar"]).foo, + Settings::default().parse(["test", "--bar"]).0.foo, SomeEnum::Bar ); assert_eq!( - Settings::default().parse(["test", "--baz"]).foo, + Settings::default().parse(["test", "--baz"]).0.foo, SomeEnum::Baz, ); } diff --git a/tests/options.rs b/tests/options.rs index d52dfe7..4a328d5 100644 --- a/tests/options.rs +++ b/tests/options.rs @@ -24,6 +24,7 @@ fn string_option() { assert_eq!( Settings::default() .parse(["test", "--message=hello"]) + .0 .message, "hello" ); @@ -60,13 +61,14 @@ fn enum_option() { } assert_eq!( - Settings::default().parse(["test", "--format=bar"]).format, + Settings::default().parse(["test", "--format=bar"]).0.format, Format::Bar ); assert_eq!( Settings::default() .parse(["test", "--format", "baz"]) + .0 .format, Format::Baz ); @@ -101,11 +103,11 @@ fn enum_option_with_fields() { } assert_eq!( - Settings::default().parse(["test", "-i=thin"]).indent, + Settings::default().parse(["test", "-i=thin"]).0.indent, Indent::Spaces(4) ); assert_eq!( - Settings::default().parse(["test", "-i=wide"]).indent, + Settings::default().parse(["test", "-i=wide"]).0.indent, Indent::Spaces(8) ); } @@ -150,11 +152,11 @@ fn enum_with_complex_from_value() { } assert_eq!( - Settings::default().parse(["test", "-i=tabs"]).indent, + Settings::default().parse(["test", "-i=tabs"]).0.indent, Indent::Tabs ); assert_eq!( - Settings::default().parse(["test", "-i=4"]).indent, + Settings::default().parse(["test", "-i=4"]).0.indent, Indent::Spaces(4) ); } @@ -190,27 +192,30 @@ fn color() { } assert_eq!( - Settings::default().parse(["test", "--color=yes"]).color, + Settings::default().parse(["test", "--color=yes"]).0.color, Color::Always ); assert_eq!( - Settings::default().parse(["test", "--color=always"]).color, + Settings::default() + .parse(["test", "--color=always"]) + .0 + .color, Color::Always ); assert_eq!( - Settings::default().parse(["test", "--color=no"]).color, + Settings::default().parse(["test", "--color=no"]).0.color, Color::Never ); assert_eq!( - Settings::default().parse(["test", "--color=never"]).color, + Settings::default().parse(["test", "--color=never"]).0.color, Color::Never ); assert_eq!( - Settings::default().parse(["test", "--color=auto"]).color, + Settings::default().parse(["test", "--color=auto"]).0.color, Color::Auto ); assert_eq!( - Settings::default().parse(["test", "--color"]).color, + Settings::default().parse(["test", "--color"]).0.color, Color::Always ) } @@ -247,7 +252,8 @@ fn actions() { } } - let settings = Settings::default().parse(["test", "-m=Hello", "-m=World", "--send"]); + let (settings, _operands) = + Settings::default().parse(["test", "-m=Hello", "-m=World", "--send"]); assert_eq!(settings.messages, vec!["Hello", "World"]); assert_eq!(settings.last_message, "World"); assert!(settings.send); @@ -275,8 +281,8 @@ fn width() { } } - assert_eq!(Settings::default().parse(["test", "-w=0"]).width, None); - assert_eq!(Settings::default().parse(["test", "-w=1"]).width, Some(1)); + assert_eq!(Settings::default().parse(["test", "-w=0"]).0.width, None); + assert_eq!(Settings::default().parse(["test", "-w=1"]).0.width, Some(1)); } #[test] @@ -327,17 +333,17 @@ fn integers() { } } - assert_eq!(Settings::default().parse(["test", "--u8=5"]).n, 5); - assert_eq!(Settings::default().parse(["test", "--u16=5"]).n, 5); - assert_eq!(Settings::default().parse(["test", "--u32=5"]).n, 5); - assert_eq!(Settings::default().parse(["test", "--u64=5"]).n, 5); - assert_eq!(Settings::default().parse(["test", "--u128=5"]).n, 5); + assert_eq!(Settings::default().parse(["test", "--u8=5"]).0.n, 5); + assert_eq!(Settings::default().parse(["test", "--u16=5"]).0.n, 5); + assert_eq!(Settings::default().parse(["test", "--u32=5"]).0.n, 5); + assert_eq!(Settings::default().parse(["test", "--u64=5"]).0.n, 5); + assert_eq!(Settings::default().parse(["test", "--u128=5"]).0.n, 5); - assert_eq!(Settings::default().parse(["test", "--i8=5"]).n, 5); - assert_eq!(Settings::default().parse(["test", "--i16=5"]).n, 5); - assert_eq!(Settings::default().parse(["test", "--i32=5"]).n, 5); - assert_eq!(Settings::default().parse(["test", "--i64=5"]).n, 5); - assert_eq!(Settings::default().parse(["test", "--i128=5"]).n, 5); + assert_eq!(Settings::default().parse(["test", "--i8=5"]).0.n, 5); + assert_eq!(Settings::default().parse(["test", "--i16=5"]).0.n, 5); + assert_eq!(Settings::default().parse(["test", "--i32=5"]).0.n, 5); + assert_eq!(Settings::default().parse(["test", "--i64=5"]).0.n, 5); + assert_eq!(Settings::default().parse(["test", "--i128=5"]).0.n, 5); } #[test] @@ -373,19 +379,20 @@ fn ls_classify() { } } - assert_eq!(Settings::default().parse(["test"]).classify, When::Auto); + assert_eq!(Settings::default().parse(["test"]).0.classify, When::Auto); assert_eq!( Settings::default() .parse(["test", "--classify=never"]) + .0 .classify, When::Never, ); assert_eq!( - Settings::default().parse(["test", "--classify"]).classify, + Settings::default().parse(["test", "--classify"]).0.classify, When::Always, ); assert_eq!( - Settings::default().parse(["test", "-F"]).classify, + Settings::default().parse(["test", "-F"]).0.classify, When::Always, ); assert!(Settings::default().try_parse(["test", "-Falways"]).is_err()); @@ -413,13 +420,13 @@ fn mktemp_tmpdir() { } } - let settings = Settings::default().parse(["test", "-p", "X"]); + let (settings, _operands) = Settings::default().parse(["test", "-p", "X"]); assert_eq!(settings.tmpdir.unwrap(), "X"); - let settings = Settings::default().parse(["test", "--tmpdir=X"]); + let (settings, _operands) = Settings::default().parse(["test", "--tmpdir=X"]); assert_eq!(settings.tmpdir.unwrap(), "X"); - let settings = Settings::default().parse(["test", "--tmpdir"]); + let (settings, _operands) = Settings::default().parse(["test", "--tmpdir"]); assert_eq!(settings.tmpdir.unwrap(), "/tmp"); assert!(Settings::default().try_parse(["test", "-p"]).is_err()); @@ -490,8 +497,8 @@ fn deprecated() { } } - assert_eq!(Settings::default().parse(["test", "-10"]).n1, 10usize); + assert_eq!(Settings::default().parse(["test", "-10"]).0.n1, 10usize); assert!(Settings::default().try_parse(["test", "--10"]).is_err()); - assert_eq!(Settings::default().parse(["test", "+10"]).n2, 10isize); - assert_eq!(Settings::default().parse(["test", "+-10"]).n2, -10isize); + assert_eq!(Settings::default().parse(["test", "+10"]).0.n2, 10isize); + assert_eq!(Settings::default().parse(["test", "+-10"]).0.n2, -10isize); } diff --git a/tests/positionals.rs b/tests/positionals.rs deleted file mode 100644 index 02eb1d4..0000000 --- a/tests/positionals.rs +++ /dev/null @@ -1,163 +0,0 @@ -use uutils_args::{Arguments, Options}; - -#[test] -fn one_positional() { - #[derive(Arguments, Clone)] - enum Arg { - #[arg("FILE", 1)] - File1(String), - } - - #[derive(Default)] - struct Settings { - file1: String, - } - - impl Options for Settings { - fn apply(&mut self, Arg::File1(f): Arg) { - self.file1 = f; - } - } - - let settings = Settings::default().parse(["test", "foo"]); - assert_eq!(settings.file1, "foo"); - - assert!(Settings::default().try_parse(["test"]).is_err()); -} - -#[test] -fn two_positionals() { - #[derive(Arguments)] - enum Arg { - #[arg("FOO", 1)] - Foo(String), - #[arg("BAR", 1)] - Bar(String), - } - - #[derive(Default)] - struct Settings { - foo: String, - bar: String, - } - - impl Options for Settings { - fn apply(&mut self, arg: Arg) { - match arg { - Arg::Foo(x) => self.foo = x, - Arg::Bar(x) => self.bar = x, - } - } - } - - let settings = Settings::default().parse(["test", "a", "b"]); - assert_eq!(settings.foo, "a"); - assert_eq!(settings.bar, "b"); - - assert!(Settings::default().try_parse(["test"]).is_err()); -} - -#[test] -fn optional_positional() { - #[derive(Arguments)] - enum Arg { - #[arg("FOO", 0..=1)] - Foo(String), - } - - #[derive(Default)] - struct Settings { - foo: Option, - } - - impl Options for Settings { - fn apply(&mut self, Arg::Foo(x): Arg) { - self.foo = Some(x); - } - } - - let settings = Settings::default().parse(["test"]); - assert_eq!(settings.foo, None); - let settings = Settings::default().parse(["test", "bar"]); - assert_eq!(settings.foo.unwrap(), "bar"); -} - -#[test] -fn collect_positional() { - #[derive(Arguments, Clone)] - enum Arg { - #[arg("FOO", ..)] - Foo(String), - } - - #[derive(Default)] - struct Settings { - foo: Vec, - } - - impl Options for Settings { - fn apply(&mut self, Arg::Foo(x): Arg) { - self.foo.push(x); - } - } - - let settings = Settings::default().parse(["test", "a", "b", "c"]); - assert_eq!(settings.foo, vec!["a", "b", "c"]); - let settings = Settings::default().parse(["test"]); - assert_eq!(settings.foo, Vec::::new()); -} - -#[test] -fn last1() { - #[derive(Arguments)] - enum Arg { - #[arg("FOO", last, ..)] - Foo(Vec), - } - - #[derive(Default)] - struct Settings { - foo: Vec, - } - - impl Options for Settings { - fn apply(&mut self, Arg::Foo(x): Arg) { - self.foo = x; - } - } - - let settings = Settings::default().parse(["test", "a", "-b", "c"]); - assert_eq!(settings.foo, vec!["a", "-b", "c"]); -} - -#[test] -fn last2() { - #[derive(Arguments, Clone)] - enum Arg { - #[arg("-a")] - A, - - #[arg("FOO", last, ..)] - Foo(Vec), - } - - #[derive(Default)] - struct Settings { - foo: Vec, - } - - impl Options for Settings { - fn apply(&mut self, arg: Arg) { - match arg { - Arg::A => {} - Arg::Foo(x) => self.foo = x, - } - } - } - - let settings = Settings::default().parse(["test", "-a"]); - assert_eq!(settings.foo, Vec::::new()); - - let settings = Settings::default().parse(["test", "--", "-a"]); - assert_eq!(settings.foo, vec!["-a"]); -} From ee746577f6d015c7e459c63f1801d5891f68ca9a Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Tue, 12 Dec 2023 15:10:55 +0100 Subject: [PATCH 036/116] add value names and hints for zsh --- complete/src/zsh.rs | 57 ++++++++++++++++++++++++++++++++++++------ derive/src/complete.rs | 4 +-- 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/complete/src/zsh.rs b/complete/src/zsh.rs index 5087697..2b709f8 100644 --- a/complete/src/zsh.rs +++ b/complete/src/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}; +use crate::{Arg, Command, Flag, Value, ValueHint}; /// Create completion script for `zsh` pub fn render(c: &Command) -> String { @@ -11,18 +11,59 @@ pub fn render(c: &Command) -> String { fn render_args(args: &[Arg]) -> String { let mut out = String::new(); let indent = " ".repeat(8); + + // The reference for this can be found here: + // https://zsh.sourceforge.io/Doc/Release/Completion-System.html#Completion-System for arg in args { let help = &arg.help; - for Flag { flag, .. } in &arg.short { - out.push_str(&format!("{indent}'-{flag}[{help}]' \\\n")); + let hint = arg + .value + .as_ref() + .map(render_value_hint) + .unwrap_or_default(); + for Flag { flag, value } in &arg.short { + let s = match value { + // No special specifier, so there might be a space in-between the flag and argument. + // The single colon means it's a required argument. + Value::Required(name) => format!("-{flag}[{help}]:{name}:{hint}"), + // '-' means that there can be no space in-between the flag and the argument + // The double colon means it's an optional argument. + Value::Optional(name) => format!("-{flag}-[{help}]::{name}:{hint}"), + Value::No => format!("-{flag}[{help}]"), + }; + out.push_str(&format!("{indent}'{s}'\\\n")); } - for Flag { flag, .. } in &arg.long { - out.push_str(&format!("{indent}'--{flag}[{help}]' \\\n")); + for Flag { flag, value } in &arg.long { + let s = match value { + // '=' means either `=` or space in-between flag and argument. + // The single colon means it's a required argument. + Value::Required(name) => format!("--{flag}=[{help}]:{name}:{hint}"), + // '=-' means that there must be a `=` for the argument. + // The double colon means it's an optional argument. + Value::Optional(name) => format!("--{flag}=-[{help}]::{name}:{hint}"), + Value::No => format!("--{flag}[{help}]"), + }; + out.push_str(&format!("{indent}'{s}' \\\n")); } } out } +fn render_value_hint(value: &ValueHint) -> String { + match value { + ValueHint::Strings(s) => { + let joined = s.join(" "); + format!("({joined})") + } + ValueHint::Unknown => "".into(), + ValueHint::AnyPath | ValueHint::FilePath => "_files".into(), + ValueHint::ExecutablePath => "_absolute_command_paths".into(), + ValueHint::DirPath => "_directories".into(), + ValueHint::Username => "_users".into(), + ValueHint::Hostname => "_hosts".into(), + } +} + fn template(name: &str, args: &str) -> String { format!( "\ @@ -35,6 +76,9 @@ _{name}() {{ typeset -a _arguments_options local ret=1 + # -s: enable option stacking + # -S: Do not complete options after a '--' appearing on the line, and ignore the '--' + # -C: Modify the curcontext parameter for an action of the form '->state' if is-at-least 5.2; then _arguments_options=(-s -S -C) else @@ -42,8 +86,7 @@ _{name}() {{ fi local context curcontext=\"$curcontext\" state line - _arguments \"${{_arguments_options[@]}}\" \\\n{args} -&& ret=0 + _arguments \"${{_arguments_options[@]}}\" \\\n{args} && ret=0 }} if [ \"$funcstack[1]\" = \"_{name}\" ]; then diff --git a/derive/src/complete.rs b/derive/src/complete.rs index 64f7d05..d7a77e4 100644 --- a/derive/src/complete.rs +++ b/derive/src/complete.rs @@ -45,7 +45,7 @@ pub fn complete(args: &[Argument], file: &Option) -> TokenStream { 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::Optional(#name)), + Value::Required(name) => quote!(::uutils_args_complete::Value::Required(#name)), }; quote!(::uutils_args_complete::Flag { flag: #flag, @@ -60,7 +60,7 @@ pub fn complete(args: &[Argument], file: &Option) -> TokenStream { 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::Optional(#name)), + Value::Required(name) => quote!(::uutils_args_complete::Value::Required(#name)), }; quote!(::uutils_args_complete::Flag { flag: #flag, From ef898fe43cb5d045e079403b67a76bc28def29b0 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Tue, 12 Dec 2023 17:12:15 +0100 Subject: [PATCH 037/116] basic nushell completions --- complete/src/lib.rs | 2 ++ complete/src/nu.rs | 51 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 complete/src/nu.rs diff --git a/complete/src/lib.rs b/complete/src/lib.rs index 15dc7ba..5d8649c 100644 --- a/complete/src/lib.rs +++ b/complete/src/lib.rs @@ -16,6 +16,7 @@ mod fish; mod man; mod md; +mod nu; mod zsh; /// A description of a CLI command @@ -72,6 +73,7 @@ pub fn render(c: &Command, shell: &str) -> String { "md" => md::render(c), "fish" => fish::render(c), "zsh" => zsh::render(c), + "nu" | "nushell" => nu::render(c), "man" => man::render(c), "sh" | "bash" | "csh" | "elvish" | "powershell" => panic!("shell '{shell}' completion is not implemented yet!"), _ => panic!("unknown option '{shell}'! Expected one of: \"md\", \"fish\", \"zsh\", \"man\", \"sh\", \"bash\", \"csh\", \"elvish\", \"powershell\""), diff --git a/complete/src/nu.rs b/complete/src/nu.rs new file mode 100644 index 0000000..95310aa --- /dev/null +++ b/complete/src/nu.rs @@ -0,0 +1,51 @@ +// 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}; +use std::fmt::Write; + +/// Create completion script for `nushell` +pub fn render(c: &Command) -> String { + let mut args = Vec::new(); + let indent = " ".repeat(4); + + for Arg { + short, + long, + help, + value: _value, + } in &c.args + { + for Flag { flag, value } in short { + let value = if let Value::Required(_) | Value::Optional(_) = value { + ": string" + } else { + "" + }; + args.push((format!("-{flag}{value}"), help)); + } + for Flag { flag, value } in long { + let value = if let Value::Required(_) | Value::Optional(_) = value { + ": string" + } else { + "" + }; + args.push((format!("--{flag}{value}"), help)); + } + } + let longest_arg = args.iter().map(|a| a.0.len()).max().unwrap_or_default(); + let mut arg_str = String::new(); + for (a, h) in args { + writeln!(arg_str, "{indent}{a: String { + format!( + "\ + export extern {name} [\n{args}\ + ]\n\ + " + ) +} From 27293fb53edafe1185cb1e7b97871ed2c0aafc14 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Wed, 13 Dec 2023 12:17:41 +0100 Subject: [PATCH 038/116] add completion example --- examples/completion.rs | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 examples/completion.rs diff --git a/examples/completion.rs b/examples/completion.rs new file mode 100644 index 0000000..22679c6 --- /dev/null +++ b/examples/completion.rs @@ -0,0 +1,42 @@ +use std::path::PathBuf; + +use uutils_args::{Arguments, Options, Value}; + +#[derive(Value)] +enum Number { + #[value] + One, + #[value] + Two, + #[value] + Three, +} + +#[derive(Arguments)] +enum Arg { + /// Give it nothing! + #[arg("-f", "--flag")] + Flag, + + // Completion is derived from the `Number` type, through the `Value` trait + /// Give it a number! + #[arg("-n N", "--number=N")] + Number(Number), + + // Completion is derived from the `PathBuf` type + /// Give it a path! + #[arg("-p P", "--path=P")] + Path(PathBuf), +} + +struct Settings; + +impl Options for Settings { + fn apply(&mut self, _arg: Arg) { + panic!("Compile with the 'parse-is-complete' feature!") + } +} + +fn main() { + Settings.parse(std::env::args_os()); +} From ff6933e863e9854b412f523cfaeb67fab5914d91 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Wed, 13 Dec 2023 12:17:56 +0100 Subject: [PATCH 039/116] nu completion: support value options --- complete/src/nu.rs | 82 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 58 insertions(+), 24 deletions(-) diff --git a/complete/src/nu.rs b/complete/src/nu.rs index 95310aa..fb9016c 100644 --- a/complete/src/nu.rs +++ b/complete/src/nu.rs @@ -1,36 +1,39 @@ // 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}; +use crate::{Arg, Command, Flag, Value, ValueHint}; use std::fmt::Write; /// Create completion script for `nushell` pub fn render(c: &Command) -> String { let mut args = Vec::new(); + let command_name = c.name; + let mut complete_commands = Vec::new(); let indent = " ".repeat(4); - for Arg { - short, - long, - help, - value: _value, - } in &c.args - { - for Flag { flag, value } in short { + for arg in &c.args { + let hint = if let Some((cmd, hint_name)) = render_completion_command(command_name, arg) { + complete_commands.push(cmd); + hint_name + } else { + "".into() + }; + + for Flag { flag, value } in &arg.short { let value = if let Value::Required(_) | Value::Optional(_) = value { - ": string" + format!(": string{hint}") } else { - "" + "".into() }; - args.push((format!("-{flag}{value}"), help)); + args.push((format!("-{flag}{value}"), arg.help)); } - for Flag { flag, value } in long { + for Flag { flag, value } in &arg.long { let value = if let Value::Required(_) | Value::Optional(_) = value { - ": string" + format!(": string{hint}") } else { - "" + "".into() }; - args.push((format!("--{flag}{value}"), help)); + args.push((format!("--{flag}{value}"), arg.help)); } } let longest_arg = args.iter().map(|a| a.0.len()).max().unwrap_or_default(); @@ -38,14 +41,45 @@ pub fn render(c: &Command) -> String { for (a, h) in args { writeln!(arg_str, "{indent}{a: Option<(String, String)> { + let val = arg.value.as_ref()?; + + // It could be that there is only a `dd` style argument. In that case, nu won't support it; + let arg_name = arg.long.first().or(arg.short.first())?.flag; + + render_value_hint(val).map(|hint| { + let name = format!("nu-complete {command_name} {arg_name}"); + let cmd = format!("def \"{name}\" [] {{\n {hint}\n}}"); + let hint_str = format!("@\"{name}\""); + (cmd, hint_str) + }) +} + +fn render_value_hint(value: &ValueHint) -> Option { + match value { + ValueHint::Strings(s) => { + let vals = s + .iter() + .map(|s| format!("\"{s}\"")) + .collect::>() + .join(", "); + Some(format!("[{vals}]")) + } + // The path arguments could be improved, but nu currently does not give + // us enough context to improve the default completions. + ValueHint::Unknown + | ValueHint::AnyPath + | ValueHint::FilePath + | ValueHint::ExecutablePath + | ValueHint::DirPath + | ValueHint::Username + | ValueHint::Hostname => None, + } } -fn template(name: &str, args: &str) -> String { - format!( - "\ - export extern {name} [\n{args}\ - ]\n\ - " - ) +fn template(name: &str, complete_commands: &str, args: &str) -> String { + format!("{complete_commands}\n\nexport extern \"{name}\" [\n{args}]\n") } From d62daf91556a3e0274a463e84203f7bc574f98b5 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Wed, 13 Dec 2023 18:53:30 +0100 Subject: [PATCH 040/116] start working on guide-level docs --- derive/src/lib.rs | 15 + .../design}/arguments_in_coreutils.md | 25 +- docs/design/design.md | 1 + design/design.md => docs/design/iterative.md | 20 +- {design => docs/design}/problems_with_clap.md | 7 +- docs/guide/completions.md | 1 + docs/guide/guide.md | 12 + docs/guide/port.md | 1 + docs/guide/quick.md | 278 ++++++++++++++++++ docs/guide/value.md | 1 + design/README.md => docs/index.md | 3 +- src/docs.rs | 24 ++ src/lib.rs | 4 + 13 files changed, 374 insertions(+), 18 deletions(-) rename {design => docs/design}/arguments_in_coreutils.md (85%) create mode 100644 docs/design/design.md rename design/design.md => docs/design/iterative.md (91%) rename {design => docs/design}/problems_with_clap.md (98%) create mode 100644 docs/guide/completions.md create mode 100644 docs/guide/guide.md create mode 100644 docs/guide/port.md create mode 100644 docs/guide/quick.md create mode 100644 docs/guide/value.md rename design/README.md => docs/index.md (97%) create mode 100644 src/docs.rs diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 4842374..5a74dfe 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -18,6 +18,21 @@ use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, Data::Enum, DeriveInput}; +/// Derive `Arguments` +/// +/// ## Argument specifications +/// +/// | specification | kind | value | +/// | -------------- | ---------- | -------- | +/// | `VAL` | positional | n.a. | +/// | `-s` | short | none | +/// | `-s VAL` | short | required | +/// | `-s[VAL]` | short | optional | +/// | `--long` | long | none | +/// | `--long=VAL` | long | required | +/// | `--long[=VAL]` | long | optional | +/// | `long=VAL` | dd | required | +/// #[proc_macro_derive(Arguments, attributes(arg, arguments))] pub fn arguments(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); diff --git a/design/arguments_in_coreutils.md b/docs/design/arguments_in_coreutils.md similarity index 85% rename from design/arguments_in_coreutils.md rename to docs/design/arguments_in_coreutils.md index 818bebc..a553511 100644 --- a/design/arguments_in_coreutils.md +++ b/docs/design/arguments_in_coreutils.md @@ -109,12 +109,25 @@ Some utils take positional arguments, which might be required. ### Deprecated syntax `+N` and `-N` -Some utils (e.g. `head`, `tail`, `kill`, `fold` and `uniq`) support an old deprecated syntax where numbers can be directly passed as arguments as a shorthand. For example, `uniq +5` is a shorthand for `uniq -s 5` and `uniq -5` is short for `uniq -f 5`. +Some utils (e.g. `head`, `tail`, `kill`, `fold` and `uniq`) support an old +deprecated syntax where numbers can be directly passed as arguments as a +shorthand. For example, `uniq +5` is a shorthand for `uniq -s 5` and `uniq -5` +is short for `uniq -f 5`. These all behave slightly differently. -1. `head` and `tail` only accept this if it is the first argument and either 1 or 2 arguments are given. -2. In `fold` the `-N` must be standalone (e.g. `-10b` is rejected), but can appear at any position. -3. In `kill`, the same rules as `fold` apply, but it can also be a name instead of a number. -4. In `uniq`, the syntax does not need to stand alone and is additive in a weird way, because they hack `-22` as `-2 -2` so each flag `-1...-9` multiplies the previous by 10 and adds itself. I'm not sure that we need to support this. Doing something like what `fold` and `kill` do is probably fine. Also note that to make it extra confusing, the `+` variant works like `fold`. + +1. `head` and `tail` only accept this if it is the first argument and either 1 + or 2 arguments are given. +2. In `fold` the `-N` must be standalone (e.g. `-10b` is rejected), but can + appear at any position. +3. In `kill`, the same rules as `fold` apply, but it can also be a name instead + of a number. +4. In `uniq`, the syntax does not need to stand alone and is additive in a weird + way, because they hack `-22` as `-2 -2` so each flag `-1...-9` multiplies the + previous by 10 and adds itself. I'm not sure that we need to support this. + Doing something like what `fold` and `kill` do is probably fine. Also note + that to make it extra confusing, the `+` variant works like `fold`. 5. `pr` the behaviour is similar to `uniq`. -6. `split` seems to be somewhere between `uniq` and `fold`. It accepts things like `-x10x` correctly, but it doesn't do the additive thing from `uniq` across multiple occurrences. Basically, it's very clever and cursed. +6. `split` seems to be somewhere between `uniq` and `fold`. It accepts things + like `-x10x` correctly, but it doesn't do the additive thing from `uniq` + across multiple occurrences. Basically, it's very clever and cursed. diff --git a/docs/design/design.md b/docs/design/design.md new file mode 100644 index 0000000..3d14cb7 --- /dev/null +++ b/docs/design/design.md @@ -0,0 +1 @@ +# Design diff --git a/design/design.md b/docs/design/iterative.md similarity index 91% rename from design/design.md rename to docs/design/iterative.md index 9754b42..2fcec1a 100644 --- a/design/design.md +++ b/docs/design/iterative.md @@ -64,10 +64,10 @@ struct Settings { > is always obvious where an argument is defined. As part of the `Options` derive, we get a `Settings::parse` method that returns -a `Settings` from a `OsString` iterator. The implementation of -this is defined by the `set` and `map` attributes. `map` just says: "if we -encounter this value in the iterator set this value", using a match-like syntax -(it expands to a match). And the `#[set(Arg::Name)]` is just short for +a `Settings` from a `OsString` iterator. The implementation of this is defined +by the `set` and `map` attributes. `map` just says: "if we encounter this value +in the iterator set this value", using a match-like syntax (it expands to a +match). And the `#[set(Arg::Name)]` is just short for `#[map(Arg::Name(name) => name)]`, because that is a commonly appearing pattern. Importantly, arguments can appear in the attributes for multiple fields. We @@ -147,7 +147,9 @@ enum Arg { ## Options struct -The options struct has just one fundamental attribute: `map`. It works much like a `match` expression (in fact, that's what it expands to). Furthermore, it's possible to define defaults on fields. +The options struct has just one fundamental attribute: `map`. It works much like +a `match` expression (in fact, that's what it expands to). Furthermore, it's +possible to define defaults on fields. ```rust #[derive(Options, Default)] @@ -180,7 +182,8 @@ struct Settings { } ``` -As a shorthand, there is also a `set` attribute. These fields behave identically: +As a shorthand, there is also a `set` attribute. These fields behave +identically: ```rust #[derive(Options, Default)] @@ -195,7 +198,8 @@ struct Settings { ## `FromValue` enums -We often want to map values to some enum, we can define this mapping by deriving `FromValue`: +We often want to map values to some enum, we can define this mapping by deriving +`FromValue`: ```rust #[derive(Default, FromValue)] @@ -210,4 +214,4 @@ enum Color { #[value("never", "no", "none")] Never, } -``` \ No newline at end of file +``` diff --git a/design/problems_with_clap.md b/docs/design/problems_with_clap.md similarity index 98% rename from design/problems_with_clap.md rename to docs/design/problems_with_clap.md index 689165c..1650c18 100644 --- a/design/problems_with_clap.md +++ b/docs/design/problems_with_clap.md @@ -111,7 +111,7 @@ uutils diverge. `clap`'s arguments are identified by strings. This leads to code like this: -```rust +```rust,ignore const OPT_NAME: &'static str = "name"; // -- snip -- @@ -183,9 +183,10 @@ libraries. - Does not support a many-to-many relationship. - [`bpaf`](https://github.com/pacak/bpaf) - Extremely flexible, even supports `dd`-style. - - A different configuration between short and long options requires a workaround. + - A different configuration between short and long options requires a + workaround. - A many-to-many relation ship is possible, though not very ergonomic. - - For more information, see: https://github.com/uutils/uutils-args/issues/17 + - For more information, see: - [`gumdrop`](https://github.com/murarth/gumdrop) - Does not handle invalid UTF-8. - Not configurable enough. diff --git a/docs/guide/completions.md b/docs/guide/completions.md new file mode 100644 index 0000000..e4228de --- /dev/null +++ b/docs/guide/completions.md @@ -0,0 +1 @@ +# Completions diff --git a/docs/guide/guide.md b/docs/guide/guide.md new file mode 100644 index 0000000..fbd7a49 --- /dev/null +++ b/docs/guide/guide.md @@ -0,0 +1,12 @@ +# Guide + +This module provides guide-level documentation for [`uutils-args`](crate). If +you're unfamiliar with this library you probably want to start with the first +chapter below and work your way through. + +## Chapters + +1. [Quick Start](guide::quick) +2. [Porting a Parser from `clap`](guide::port) +3. [The `Value` trait](`guide::value`) +4. [Generating completions](`guide::completions`) diff --git a/docs/guide/port.md b/docs/guide/port.md new file mode 100644 index 0000000..c7eaa61 --- /dev/null +++ b/docs/guide/port.md @@ -0,0 +1 @@ +# Porting from Clap diff --git a/docs/guide/quick.md b/docs/guide/quick.md new file mode 100644 index 0000000..ba3ea51 --- /dev/null +++ b/docs/guide/quick.md @@ -0,0 +1,278 @@ +# Quick Start + +A parser consists of two parts: + +- an `enum` implementing [`Arguments`](crate::Arguments) +- an `struct` implementing [`Options`](crate::Options) + +The `enum` defines all the arguments that your application accepts. The `struct` represents all configuration options for the application. In other words, the `struct` is the internal representation of the options, while the `enum` is the external representation. + +## A single flag + +We can create arguments by annotating a variant of an `enum` deriving [`Arguments`](crate::Arguments) with the `arg` attribute. This attribute takes strings that define the arguments. A short flag, for instance, looks like `"-f"` and a long flag looks like `"--flag"`. The full syntax for the arguments specifications can be found in the documentation for the [`Arguments` derive macro](derive@crate::Arguments) + +To represent the program configuration we create a struct called `Settings`, which implements `Options`. When an argument is encountered, we _apply_ it to the `Settings` struct. In this case, we set the `force` field of `Settings` to `true` if `Arg::Force` is parsed. + +```rust +use uutils_args::{Arguments, Options}; + +#[derive(Arguments)] +enum Arg { + #[arg("-f", "--force")] + Force, +} + +// Note: Debug, PartialEq and Eq are only necessary for assert_eq! below. +#[derive(Default, Debug, PartialEq, Eq)] +struct Settings { + force: bool +} + +impl Options for Settings { + fn apply(&mut self, arg: Arg) { + match arg { + Arg::Force => self.force = true, + } + } +} + +assert_eq!(Settings::default().parse(["test"]), Settings { force: false }); +assert_eq!(Settings::default().parse(["test", "-f"]), Settings { force: true }); +``` + +## Two overriding flags + +Of course, we can define multiple flags. If these arguments change the same fields of `Settings`, then they will override. This is important: by default none of the arguments will "conflict", they will always simply be processed in order. + +```rust +use uutils_args::{Arguments, Options}; + +#[derive(Arguments)] +enum Arg { + #[arg("-f", "--force")] + Force, + #[arg("-F", "--no-force")] + NoForce, +} + +// Note: Debug, PartialEq and Eq are only necessary for assert_eq! below. +#[derive(Default, Debug, PartialEq, Eq)] +struct Settings { + force: bool +} + +impl Options for Settings { + fn apply(&mut self, arg: Arg) { + match arg { + Arg::Force => self.force = true, + Arg::NoForce => self.force = false, + } + } +} + +assert_eq!(Settings::default().parse(["test"]), Settings { force: false }); +assert_eq!(Settings::default().parse(["test", "-f"]), Settings { force: true }); +assert_eq!(Settings::default().parse(["test", "-F"]), Settings { force: false }); +``` + +## Help strings + +We can document our flags in two ways: by giving them a docstring or by giving the `arg` attribute a `help` argument. Note that the `help` argument will take precedence over the docstring. + +```rust +use uutils_args::Arguments; + +#[derive(Arguments)] +enum Arg { + /// Force! + #[arg("-f", "--force")] + Force, + #[arg("-F", "--no-force", help = "No! Don't force!")] + NoForce, +} +``` + +## Arguments with required values + +So far, our arguments have been simple flags that do not take any arguments, but `uutils-args` supports much more! If we want an argument for our option, the corresponding variant on our `enum` needs to take an argument too. + +> **Note**: In the example below, we use `OsString`. A regular `String` works too, but is generally discouraged in `coreutils`, because we often have to support text with invalid UTF-8. + +```rust +# use uutils_args::{Arguments, Options}; +# use std::ffi::OsString; +# +#[derive(Arguments)] +enum Arg { + #[arg("-n NAME", "--name=NAME")] + Name(OsString), +} +# +# // Note: Debug, PartialEq and Eq are only necessary for assert_eq! below. +# #[derive(Default, Debug, PartialEq, Eq)] +# struct Settings { +# name: OsString +# } +# +# impl Options for Settings { +# fn apply(&mut self, arg: Arg) { +# match arg { +# Arg::Name(name) => self.name = name, +# } +# } +# } +# +# assert_eq!( +# Settings::default().parse(["test"]), +# Settings { name: OsString::new() } +# ); +# assert_eq!( +# Settings::default().parse(["test", "--name=John"]), +# Settings { name: OsString::from("John")} +# ); +``` + +## Arguments with optional values + +Arguments with optional values are possible, too. However, we have to give a value to be used if the value is not given. Below, we set that value to `OsString::from("anonymous")`, with the `value` argument of `arg`. + +```rust +# use uutils_args::{Arguments, Options}; +# use std::ffi::OsString; +# +#[derive(Arguments)] +enum Arg { + #[arg("-n[NAME]", "--name[=NAME]", value = OsString::from("anonymous"))] + Name(OsString), +} +# +# #[derive(Default, Debug, PartialEq, Eq)] +# struct Settings { +# name: OsString +# } +# +# impl Options for Settings { +# fn apply(&mut self, arg: Arg) { +# match arg { +# Arg::Name(name) => self.name = name, +# } +# } +# } +# +# assert_eq!( +# Settings::default().parse(["test", "--name"]), +# Settings { name: OsString::from("anonymous")} +# ); +# assert_eq!( +# Settings::default().parse(["test", "--name=John"]), +# Settings { name: OsString::from("John")} +# ); +``` + +## Multiple arguments per variant + +Here's a neat trick: you can use multiple `arg` attributes per variant. Recall the `--force/--no-force` example above. We could have written that as follows: + +```rust +# use uutils_args::{Arguments, Options}; +# +#[derive(Arguments)] +enum Arg { + #[arg("-f", "--force", value = true, help = "enable force")] + #[arg("-F", "--no-force", value = false, help = "disable force")] + Force(bool), +} +# +# #[derive(Default, Debug, PartialEq, Eq)] +# struct Settings { +# force: bool +# } +# +# impl Options for Settings { +# fn apply(&mut self, arg: Arg) { +# match arg { +# Arg::Force(b) => self.force = b, +# } +# } +# } +# +# assert_eq!(Settings::default().parse(["test"]), Settings { force: false }); +# assert_eq!(Settings::default().parse(["test", "-f"]), Settings { force: true }); +# assert_eq!(Settings::default().parse(["test", "-F"]), Settings { force: false }); +``` + +This is particularly interesting for defining "shortcut" arguments. For example, `ls` takes a `--sort=WORD` argument, that defines how the files should be sorted. But it also has shorthands like `-t`, which is the same as `--sort=time`. All of these can be implemented on one variant: + +> **Note**: The `--sort` argument should not take a `String` as value. We've done that here for illustrative purposes. It should actually use an `enum` with the `Value` trait. + +```rust +# use uutils_args::{Arguments, Options}; +# +#[derive(Arguments)] +enum Arg { + #[arg("--sort=WORD", help = "Sort by WORD")] + #[arg("-t", value = String::from("time"), help = "Sort by time")] + #[arg("-U", value = String::from("none"), help = "Do not sort")] + #[arg("-v", value = String::from("version"), help = "Sort by version")] + #[arg("-X", value = String::from("extension"), help = "Sort by extension")] + Sort(String), +} +# +# #[derive(Default, Debug, PartialEq, Eq)] +# struct Settings { +# sort: String +# } +# +# impl Options for Settings { +# fn apply(&mut self, arg: Arg) { +# match arg { +# Arg::Sort(s) => self.sort = s, +# } +# } +# } +# +# assert_eq!(Settings::default().parse(["test"]), Settings { sort: String::new() }); +# assert_eq!(Settings::default().parse(["test", "--sort=time"]), Settings { sort: String::from("time") }); +# assert_eq!(Settings::default().parse(["test", "-t"]), Settings { sort: String::from("time") }); +``` + +## Positional arguments + +Finally, at the end of this whirlwind tour, we get to positional arguments. Here's a simple positional argument: + +```rust +use uutils_args::{Arguments, Options}; +use std::path::PathBuf; + +#[derive(Arguments)] +enum Arg { + #[arg("FILES", ..)] + File(PathBuf) +} + +#[derive(Default, Debug, PartialEq, Eq)] +struct Settings { + files: Vec, +} + +impl Options for Settings { + fn apply(&mut self, arg: Arg) { + match arg { + Arg::File(f) => self.files.push(f), + } + } +} +# +# assert_eq!( +# Settings::default().parse(["test"]), +# Settings { files: Vec::new() } +# ); +# assert_eq!( +# Settings::default().parse(["test", "foo"]), +# Settings { files: vec![PathBuf::from("foo")] } +# ); +# assert_eq!( +# Settings::default().parse(["test", "foo", "bar"]), +# Settings { files: vec!["foo".into(), "bar".into()] } +# ); +``` \ No newline at end of file diff --git a/docs/guide/value.md b/docs/guide/value.md new file mode 100644 index 0000000..4c689c8 --- /dev/null +++ b/docs/guide/value.md @@ -0,0 +1 @@ +# Value trait diff --git a/design/README.md b/docs/index.md similarity index 97% rename from design/README.md rename to docs/index.md index 164bca4..22b94bf 100644 --- a/design/README.md +++ b/docs/index.md @@ -14,7 +14,8 @@ decisions. Before diving in, let's lay out the design goals of this project. fewer features to support. - Use outside uutils is possible but not prioritized. Hence, configurability beyond the coreutils is not necessary. -- Errors must be at least as good as GNU's, but may be different (hopefully improved). +- Errors must be at least as good as GNU's, but may be different (hopefully + improved). ## Pages diff --git a/src/docs.rs b/src/docs.rs new file mode 100644 index 0000000..3168ade --- /dev/null +++ b/src/docs.rs @@ -0,0 +1,24 @@ +//! This module contains only documentation to be rendered by rustdoc. +//! +//! - [Guide](guide): the guide for using this library +//! - [Design](design): documents about the design of this library + +#[doc = include_str!("../docs/guide/guide.md")] +pub mod guide { + #[doc = include_str!("../docs/guide/quick.md")] + pub mod quick {} + #[doc = include_str!("../docs/guide/port.md")] + pub mod port {} + #[doc = include_str!("../docs/guide/completions.md")] + pub mod completions {} + #[doc = include_str!("../docs/guide/value.md")] + pub mod value {} +} + +#[doc = include_str!("../docs/design/design.md")] +pub mod design { + #[doc = include_str!("../docs/design/arguments_in_coreutils.md")] + pub mod coreutils {} + #[doc = include_str!("../docs/design/problems_with_clap.md")] + pub mod problems {} +} diff --git a/src/lib.rs b/src/lib.rs index 5f4ecc5..87716a1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,16 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +//! [Click here to check out the guide-level documentation](docs::guide) #![doc = include_str!("../README.md")] mod error; pub mod internal; mod value; +#[cfg(doc)] +pub mod docs; + pub use lexopt; pub use uutils_args_derive::*; From 757214b6bebec4e7da77ea525b1219c437fc0538 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Wed, 13 Dec 2023 19:01:26 +0100 Subject: [PATCH 041/116] do not require value_hint when none of the arguments use it --- derive/src/complete.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/derive/src/complete.rs b/derive/src/complete.rs index d7a77e4..10b9691 100644 --- a/derive/src/complete.rs +++ b/derive/src/complete.rs @@ -38,6 +38,12 @@ pub fn complete(args: &[Argument], file: &Option) -> TokenStream { continue; } + // If none of the flags take an argument, we won't need ValueHint + // based on that type. So we should not attempt to call `value_hint` + // on it. + let any_flag_takes_argument = + short.iter().any(|f| f.value != Value::No) && long.iter().any(|f| f.value != Value::No); + let short: Vec<_> = short .iter() .map(|Flag { flag, value }| { @@ -69,10 +75,9 @@ pub fn complete(args: &[Argument], file: &Option) -> TokenStream { }) .collect(); - let hint = if let Some(ty) = field { - quote!(Some(<#ty>::value_hint())) - } else { - quote!(None) + let hint = match (field, any_flag_takes_argument) { + (Some(ty), true) => quote!(Some(<#ty>::value_hint())), + _ => quote!(None), }; arg_specs.push(quote!( From d67f2cd250cb08c11e897dca36465551d61d692b Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Thu, 14 Dec 2023 11:39:58 +0100 Subject: [PATCH 042/116] clean up assertions for quick start --- docs/guide/quick.md | 63 +++++++++++++++++++++------------------------ 1 file changed, 30 insertions(+), 33 deletions(-) diff --git a/docs/guide/quick.md b/docs/guide/quick.md index ba3ea51..5f5a12d 100644 --- a/docs/guide/quick.md +++ b/docs/guide/quick.md @@ -22,8 +22,7 @@ enum Arg { Force, } -// Note: Debug, PartialEq and Eq are only necessary for assert_eq! below. -#[derive(Default, Debug, PartialEq, Eq)] +#[derive(Default)] struct Settings { force: bool } @@ -36,8 +35,8 @@ impl Options for Settings { } } -assert_eq!(Settings::default().parse(["test"]), Settings { force: false }); -assert_eq!(Settings::default().parse(["test", "-f"]), Settings { force: true }); +assert!(!Settings::default().parse(["test"]).force); +assert!(Settings::default().parse(["test", "-f"]).force); ``` ## Two overriding flags @@ -55,8 +54,7 @@ enum Arg { NoForce, } -// Note: Debug, PartialEq and Eq are only necessary for assert_eq! below. -#[derive(Default, Debug, PartialEq, Eq)] +#[derive(Default)] struct Settings { force: bool } @@ -70,9 +68,9 @@ impl Options for Settings { } } -assert_eq!(Settings::default().parse(["test"]), Settings { force: false }); -assert_eq!(Settings::default().parse(["test", "-f"]), Settings { force: true }); -assert_eq!(Settings::default().parse(["test", "-F"]), Settings { force: false }); +assert!(!Settings::default().parse(["test"]).force); +assert!(Settings::default().parse(["test", "-f"]).force); +assert!(!Settings::default().parse(["test", "-f", "-F"]).force); ``` ## Help strings @@ -108,8 +106,7 @@ enum Arg { Name(OsString), } # -# // Note: Debug, PartialEq and Eq are only necessary for assert_eq! below. -# #[derive(Default, Debug, PartialEq, Eq)] +# #[derive(Default)] # struct Settings { # name: OsString # } @@ -123,12 +120,12 @@ enum Arg { # } # # assert_eq!( -# Settings::default().parse(["test"]), -# Settings { name: OsString::new() } +# Settings::default().parse(["test"]).name, +# OsString::new(), # ); # assert_eq!( -# Settings::default().parse(["test", "--name=John"]), -# Settings { name: OsString::from("John")} +# Settings::default().parse(["test", "--name=John"]).name, +# OsString::from("John"), # ); ``` @@ -160,12 +157,12 @@ enum Arg { # } # # assert_eq!( -# Settings::default().parse(["test", "--name"]), -# Settings { name: OsString::from("anonymous")} +# Settings::default().parse(["test", "--name"]).name, +# OsString::from("anonymous"), # ); # assert_eq!( -# Settings::default().parse(["test", "--name=John"]), -# Settings { name: OsString::from("John")} +# Settings::default().parse(["test", "--name=John"]).name, +# OsString::from("John"), # ); ``` @@ -183,7 +180,7 @@ enum Arg { Force(bool), } # -# #[derive(Default, Debug, PartialEq, Eq)] +# #[derive(Default)] # struct Settings { # force: bool # } @@ -196,9 +193,9 @@ enum Arg { # } # } # -# assert_eq!(Settings::default().parse(["test"]), Settings { force: false }); -# assert_eq!(Settings::default().parse(["test", "-f"]), Settings { force: true }); -# assert_eq!(Settings::default().parse(["test", "-F"]), Settings { force: false }); +# assert!(!Settings::default().parse(["test"]).force); +# assert!(Settings::default().parse(["test", "-f"]).force); +# assert!(!Settings::default().parse(["test", "-F"]).force); ``` This is particularly interesting for defining "shortcut" arguments. For example, `ls` takes a `--sort=WORD` argument, that defines how the files should be sorted. But it also has shorthands like `-t`, which is the same as `--sort=time`. All of these can be implemented on one variant: @@ -218,7 +215,7 @@ enum Arg { Sort(String), } # -# #[derive(Default, Debug, PartialEq, Eq)] +# #[derive(Default)] # struct Settings { # sort: String # } @@ -231,9 +228,9 @@ enum Arg { # } # } # -# assert_eq!(Settings::default().parse(["test"]), Settings { sort: String::new() }); -# assert_eq!(Settings::default().parse(["test", "--sort=time"]), Settings { sort: String::from("time") }); -# assert_eq!(Settings::default().parse(["test", "-t"]), Settings { sort: String::from("time") }); +# assert_eq!(Settings::default().parse(["test"]).sort, String::new()); +# assert_eq!(Settings::default().parse(["test", "--sort=time"]).sort, String::from("time")); +# assert_eq!(Settings::default().parse(["test", "-t"]).sort, String::from("time")); ``` ## Positional arguments @@ -264,15 +261,15 @@ impl Options for Settings { } # # assert_eq!( -# Settings::default().parse(["test"]), -# Settings { files: Vec::new() } +# Settings::default().parse(["test"]).files, +# Vec::::new(), # ); # assert_eq!( -# Settings::default().parse(["test", "foo"]), -# Settings { files: vec![PathBuf::from("foo")] } +# Settings::default().parse(["test", "foo"]).files, +# vec![PathBuf::from("foo")], # ); # assert_eq!( -# Settings::default().parse(["test", "foo", "bar"]), -# Settings { files: vec!["foo".into(), "bar".into()] } +# Settings::default().parse(["test", "foo", "bar"]).files, +# vec![PathBuf::from("foo"), PathBuf::from("bar")], # ); ``` \ No newline at end of file From 8adaad0565c379987a10995e58f9bd7887c91b1c Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Thu, 14 Dec 2023 14:32:01 +0100 Subject: [PATCH 043/116] start writing guide for porting from clap --- docs/guide/port.md | 238 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 238 insertions(+) diff --git a/docs/guide/port.md b/docs/guide/port.md index c7eaa61..c99739a 100644 --- a/docs/guide/port.md +++ b/docs/guide/port.md @@ -1 +1,239 @@ # Porting from Clap + +This chapter contains information about how common patterns in `clap` parsers can be ported to `uutils-args`. + +More examples can be added here while we figure out more common patterns. + +## Defaults + +By default, the `clap` command roughly equivalent to a command from `uutils-args` looks like this (where everything with `...` is filled in automatically). + +```rust,ignore +Command::new(...) + .version(...) + .override_usage(...) + .about(...) + .infer_long_args(true) + .args_override_self(true) + .disable_help_flag(true) + .disable_version_flag(true) + .arg( + Arg::new("help") + .long("help") + .help("Print help information.") + .action(ArgAction::Help), + ) + .arg( + Arg::new("version") + .long("version") + .help("Print version information.") + .action(ArgAction::Version), + ) +``` + +Further differences are: + +- Overrides are the default in `uutils-args`. There is no automatic conflict checking. +- Values can always start with hyphens. +- Long flags with optional arguments always require an equal sign. + +## `ArgAction` equivalents + +### `ArgAction::SetTrue` + +```rust,ignore +let command = Command::new(...) + .arg( + Arg::new("a") + .short('a') + .action(ArgAction::SetTrue) + ); + +let matches = command.get_matches(); + +let a = matches.get_flag("a"); +``` + +```rust +use uutils_args::{Arguments, Options}; + +#[derive(Arguments)] +enum Arg { + #[arg("-a")] + A +} + +#[derive(Default)] +struct Settings { a: bool } + +impl Options for Settings { + fn apply(&mut self, arg: Arg) { + match arg { + Arg::A => self.a = true, + } + } +} + +let a = Settings::default().parse(std::env::args_os()).a; +``` + +### `ArgAction::SetFalse` + +```rust,ignore +let command = Command::new(...) + .arg( + Arg::new("a") + .short('a') + .action(ArgAction::SetFalse) + ); + +let matches = command.get_matches(); + +let a = matches.get_flag("a"); +``` + +```rust +use uutils_args::{Arguments, Options}; + +#[derive(Arguments)] +enum Arg { + #[arg("-a")] + A +} + +struct Settings { a: bool } + +impl Default for Settings { + fn default() -> Self { + Self { a: false } + } +} + +impl Options for Settings { + fn apply(&mut self, arg: Arg) { + match arg { + Arg::A => self.a = false, + } + } +} + +let a = Settings::default().parse(std::env::args_os()).a; +``` + +### `ArgAction::Count` + +```rust,ignore +let command = Command::new(...) + .arg( + Arg::new("a") + .short('a') + .action(ArgAction::Count) + ); + +let matches = command.get_matches(); + +let a = matches.get_one("a").unwrap(); +``` + +```rust +use uutils_args::{Arguments, Options}; + +#[derive(Arguments)] +enum Arg { + #[arg("-a")] + A +} + +#[derive(Default)] +struct Settings { a: u8 } + +impl Options for Settings { + fn apply(&mut self, arg: Arg) { + match arg { + Arg::A => self.a += 1, + } + } +} + +let a = Settings::default().parse(std::env::args_os()).a; +``` + +### `ArgAction::Set` + +```rust,ignore +let command = Command::new(...) + .arg( + Arg::new("a") + .short('a') + .action(ArgAction::Set) + .value_name("VAL") + ); + +let matches = command.get_matches(); + +let a = matches.get_one("a").unwrap(); +``` + +```rust +use uutils_args::{Arguments, Options}; +use std::ffi::OsString; + +#[derive(Arguments)] +enum Arg { + #[arg("-a VAL")] + A(OsString) +} + +#[derive(Default)] +struct Settings { a: OsString } + +impl Options for Settings { + fn apply(&mut self, arg: Arg) { + match arg { + Arg::A(s) => self.a = s, + } + } +} + +let a = Settings::default().parse(std::env::args_os()).a; +``` + +### `ArgAction::Append` + +```rust,ignore +let command = Command::new(...) + .arg( + Arg::new("a") + .short('a') + .action(ArgAction::Append) + .value_name("VAL") + ); + +let matches = command.get_matches(); + +let a = matches.get_one("a").unwrap(); +``` + +```rust +use uutils_args::{Arguments, Options}; +use std::ffi::OsString; + +#[derive(Arguments)] +enum Arg { + #[arg("-a VAL")] + A(OsString) +} + +#[derive(Default)] +struct Settings { a: Vec } + +impl Options for Settings { + fn apply(&mut self, arg: Arg) { + match arg { + Arg::A(s) => self.a.push(s), + } + } +} + +let a = Settings::default().parse(std::env::args_os()).a; +``` From c8dc4793a0b7432e318d97c528c60e3ee08bb41c Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Thu, 14 Dec 2023 15:32:32 +0100 Subject: [PATCH 044/116] expand design documentation --- docs/design/design.md | 7 + docs/design/iterative.md | 217 ------------------------------ docs/design/problems_with_clap.md | 13 +- 3 files changed, 14 insertions(+), 223 deletions(-) delete mode 100644 docs/design/iterative.md diff --git a/docs/design/design.md b/docs/design/design.md index 3d14cb7..2944283 100644 --- a/docs/design/design.md +++ b/docs/design/design.md @@ -1 +1,8 @@ # Design + +This module contains some documents about the design of this library. In particular, it details the different kinds of arguments that are present in the coreutils and the difficulties that `clap` presents when implementing these arguments. + +## Chapters + +1. [Arguments in the coreutils](design::coreutils) +2. [Problems with `clap`](design::problems) diff --git a/docs/design/iterative.md b/docs/design/iterative.md deleted file mode 100644 index 2fcec1a..0000000 --- a/docs/design/iterative.md +++ /dev/null @@ -1,217 +0,0 @@ -# Library design - -In this document, I explain how this library solves the problems with `clap` and -how it accomplishes the design goals. - -## Basic API - -This library only has a derive API. In most derive-based argument parsers, the -arguments are based on a `struct`, but in this library they are based on `enum` -variants, which then get mapped to a `struct`. The parsing happens in two stages - -1. Arguments get mapped to an `enum` -2. The `enum` variants are matched and update `struct` fields. - -This gives us a separation of concerns: the `enum` determines how the arguments -get parsed and the `struct` determines how they map to the program settings. -This gives us a lot of freedom in defining our mapping from arguments to -settings. - -Here is a simple example comparing `clap` and `uutils_args`. - -> **Note**: There are differences in behaviour between these two. E.g. -> uutils_args allows options to appear multiple times, remembering only the last -> one. - -```rust -// Clap -#[derive(Parser)] -struct Args { - /// Name of the person to greet - #[arg(short, long)] - name: String, - - /// Number of times to greet - #[arg(short, long)] - say_goodbye: bool, -} - -// Uutils args -#[derive(Arguments, Clone)] -enum Arg { - /// Name of the person to greet - #[option("-n NAME", "--name=NAME")] - Name(String), - - /// Number of times to greet - #[option("-g", "--goodbye")] - SayGoodbye -} - -#[derive(Options, Default)] -#[arg_type(Arg)] -struct Settings { - #[set(Arg::Name)] - name: String - - #[map(Arg::SayGoodbye => true)] - goodbye: bool, -} -``` - -> **Note**: `uutils_args` is more explicit than `clap`, you have to explicitly -> state the names of the flags and values. This helps maintainability because it -> is always obvious where an argument is defined. - -As part of the `Options` derive, we get a `Settings::parse` method that returns -a `Settings` from a `OsString` iterator. The implementation of this is defined -by the `set` and `map` attributes. `map` just says: "if we encounter this value -in the iterator set this value", using a match-like syntax (it expands to a -match). And the `#[set(Arg::Name)]` is just short for -`#[map(Arg::Name(name) => name)]`, because that is a commonly appearing pattern. - -Importantly, arguments can appear in the attributes for multiple fields. We -could for instance do this: - -```rust -#[derive(Arguments, Clone)] -enum Arg { - #[option("-a")] - A, - - #[option("--a-and-b")] - B -} - -#[derive(Options, Default)] -#[arg_type(Arg)] -struct Settings { - #[map(Arg::A | Arg::B => true)] - a: bool - - #[map(Arg::B => true)] - b: bool, -} -``` - -## Argument types - -```rust -#[derive(Arguments, Clone)] -#[help("--help")] // help and version must be explicitly defined -#[version("--version")] -enum Arg { - // Note: You can have as many flags as you want for each variable - #[option("-f", "--foo")] - Flag, - - // Note: The value name is required and will be used in `--help` - #[option("-r VALUE", "--required=VALUE")] - OptionWithRequiredValue(String), - - // Note: The value name is again required. - // Note: If no `default` is specified, `Default::default` is used. - #[option("-o[VALUE]", "--optional[=VALUE]", default = "DEFAULT".into())] - OptionWithOptionalValue(String), - - // Note: `-l` will use the default value. - #[option("-l", "--long=VALUE", default = "SHORT VALUE")] - ValueOnlyForLongOption(String), - - // Any combination of required, optional and no arguments is possible. - #[option("-t VAL", "--test[=VAL]", default = "")] - ValueOptionalForLongOption(String), - - // Positional arguments take a range of the number of arguments they - // take. The default is 1..=1, i.e. exactly 1 argument. - #[positional] - SinglePositionalArgument(String), - - #[positional(0..=1)] - OptionalPositionalArgument(String), - - // Range is open on both sides so 0..=MAX - #[positional(..)] - AnyNumberOfPositionalArguments(String), - - // All remaining arguments are collected into a `Vec`. - #[position(last)] - TrailingVarArg(Vec), - - // Same range can still be applied even though there can only ever - // be 1 trailing var arg. - #[position(last, 0..=1)] - OptionalTrailingVarArg(Vec), -} -``` - -## Options struct - -The options struct has just one fundamental attribute: `map`. It works much like -a `match` expression (in fact, that's what it expands to). Furthermore, it's -possible to define defaults on fields. - -```rust -#[derive(Options, Default)] -struct Settings { - // When a Arg::Foo is parsed, set this field to `true`. - // Any expression is possible. - // Any field starts with `Default::default()`. - #[map(Arg::Foo => true)] - foo: bool - - // Arg::BarTrue sets this to true, Arg::BarFalse sets this to false. - // We can have as many arms as we want. For each field, the first - // matching arm is applied and the rest is ignored. - #[map( - Arg::BarTrue => true, - Arg::BarFalse => false, - )] - bar: bool, - - // We can set a default value with the field attribute. - #[map(Arg::Baz => false)] - #[field(default = true)] - baz: bool, - - // We can also define a env var to read from if available, else - // the default value will be used. - #[map(Arg::SomeVar => true)] - #[field(env = "SOME_VAR", default = false)] - some_var: bool, -} -``` - -As a shorthand, there is also a `set` attribute. These fields behave -identically: - -```rust -#[derive(Options, Default)] -struct Settings { - #[map(Arg::Foo(f) => f)] - bar: u64, - - #[set(Arg::Foo)] - baz: u64 -} -``` - -## `FromValue` enums - -We often want to map values to some enum, we can define this mapping by deriving -`FromValue`: - -```rust -#[derive(Default, FromValue)] -enum Color { - #[value("always", "yes", "force")] - Always, - - #[default] - #[value("auto", "tty", "if-tty")] - Auto, - - #[value("never", "no", "none")] - Never, -} -``` diff --git a/docs/design/problems_with_clap.md b/docs/design/problems_with_clap.md index 1650c18..d3c8774 100644 --- a/docs/design/problems_with_clap.md +++ b/docs/design/problems_with_clap.md @@ -8,9 +8,9 @@ inspiration from them. Before I continue, I want to note that these are not (always) general problems with `clap`. They are problems that show up when you want to implement the coreutils with it. The coreutils have some weird behaviour that you won't have -to deal with in a new project. `clap` is still a really good library and you +to deal with in a new project. `clap` is still a great library, and you should probably use it over this library, unless you need compatibility with GNU -utils. +utilities. ## Problem 1: No many-to-many relationship between arguments and settings @@ -18,7 +18,7 @@ This is the biggest issue we have with `clap`. In `clap`, it is assumed that options do not interfere with each other. This means that _partially overriding_ options are really hard to support. `rm` has `--interactive` and `-f`, which mostly just override each other, because they set the interactive mode and -decide whether to print warnings. However, `--interactive=never` does nog change +decide whether to print warnings. However, `--interactive=never` does not change whether warnings are printed. Hence, they cannot override completely, because then these two are **not** identical: @@ -59,9 +59,10 @@ Changing these defaults is sometimes just a single line, but other times it becomes quite verbose. In particular, setting the options to override becomes quite verbose in some cases. -[^1]: There is a setting to set it for all arguments, but it behaves differently -than setting it individually and leads to some troubles, due to the differences -mentioned in the next section. +[^1]: + There is a setting to set it for all arguments, but it behaves differently + than setting it individually and leads to some troubles, due to the differences + mentioned in the next section. ## Problem 4: Subtle differences From 04131a41c8b05aaa5a93fdf60c550f40c0b5e480 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Thu, 14 Dec 2023 15:32:42 +0100 Subject: [PATCH 045/116] document completion generation --- docs/guide/completions.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/guide/completions.md b/docs/guide/completions.md index e4228de..efecf40 100644 --- a/docs/guide/completions.md +++ b/docs/guide/completions.md @@ -1 +1,15 @@ # Completions + +Shell completions and documentation can be generated automatically by this crate. The implementation for this is in the [`uutils-args-complete`] crate. The easiest way of generating completions is via the `parse-is-complete` feature flag. This feature flag hijacks the [`Options::parse`](crate::Options::parse) function to print completions. This means that there is usually no need to write any additional code to generate completions. + +```bash +cargo run --features parse-is-complete -- [shell] +``` + +The `[shell]` value here can be `fish`, `zsh`, `bash`, `powershell`, `elvish` or `nu`. + +> **Note**: Some of these remain unimplemented as of writing. + +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. From 3214c735340978c0f5e5fd81216d9a19e91063f6 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Thu, 14 Dec 2023 15:36:57 +0100 Subject: [PATCH 046/116] move information from index.md to design.md --- docs/design/design.md | 16 ++++++++++++++++ docs/index.md | 24 ------------------------ 2 files changed, 16 insertions(+), 24 deletions(-) delete mode 100644 docs/index.md diff --git a/docs/design/design.md b/docs/design/design.md index 2944283..c79b192 100644 --- a/docs/design/design.md +++ b/docs/design/design.md @@ -2,6 +2,22 @@ This module contains some documents about the design of this library. In particular, it details the different kinds of arguments that are present in the coreutils and the difficulties that `clap` presents when implementing these arguments. +The primary design considerations of this library are: + +- Must support all options in GNU coreutils. +- Must support a many-to-many relationship between options and settings. +- Must have a convenient derive API. +- Must support help strings from file. +- Code must be "greppable" (e.g. search file for `--all` to find the code for + that argument). +- Maintainability is more important than terseness. +- With a bit of luck, it will be smaller and faster than `clap`, because we have + fewer features to support. +- Use outside uutils is possible but not prioritized. Hence, configurability + beyond the coreutils is not necessary. +- Errors must be at least as good as GNU's, but may be different (hopefully + improved). + ## Chapters 1. [Arguments in the coreutils](design::coreutils) diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 22b94bf..0000000 --- a/docs/index.md +++ /dev/null @@ -1,24 +0,0 @@ -# `uutils-args` Design Docs - -This is a series of design documents, explaining the various design goals and -decisions. Before diving in, let's lay out the design goals of this project. - -- Must support all options in GNU coreutils. -- Must support a many-to-many relationship between options and settings. -- Must have a convenient derive API. -- Must support help strings from file. -- Code must be "greppable" (e.g. search file for `--all` to find the code for - that argument). -- Maintainability is more important than terseness. -- With a bit of luck, it will be smaller and faster than `clap`, because we have - fewer features to support. -- Use outside uutils is possible but not prioritized. Hence, configurability - beyond the coreutils is not necessary. -- Errors must be at least as good as GNU's, but may be different (hopefully - improved). - -## Pages - -1. [Arguments in coreutils](arguments_in_coreutils.md) -2. [Problems with `clap` and other parsers](problems_with_clap.md) -3. [Library design](design.md) (TODO once the design settles) From 50cd4b14941568d2a30d15418e03d7597a9e8c82 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Tue, 19 Dec 2023 12:30:45 +0100 Subject: [PATCH 047/116] update docs to new positional arguments --- docs/guide/port.md | 10 ++--- docs/guide/quick.md | 92 ++++++++++++++++++--------------------------- 2 files changed, 41 insertions(+), 61 deletions(-) diff --git a/docs/guide/port.md b/docs/guide/port.md index c99739a..a154a10 100644 --- a/docs/guide/port.md +++ b/docs/guide/port.md @@ -74,7 +74,7 @@ impl Options for Settings { } } -let a = Settings::default().parse(std::env::args_os()).a; +let a = Settings::default().parse(std::env::args_os()).0.a; ``` ### `ArgAction::SetFalse` @@ -117,7 +117,7 @@ impl Options for Settings { } } -let a = Settings::default().parse(std::env::args_os()).a; +let a = Settings::default().parse(std::env::args_os()).0.a; ``` ### `ArgAction::Count` @@ -155,7 +155,7 @@ impl Options for Settings { } } -let a = Settings::default().parse(std::env::args_os()).a; +let a = Settings::default().parse(std::env::args_os()).0.a; ``` ### `ArgAction::Set` @@ -195,7 +195,7 @@ impl Options for Settings { } } -let a = Settings::default().parse(std::env::args_os()).a; +let a = Settings::default().parse(std::env::args_os()).0.a; ``` ### `ArgAction::Append` @@ -235,5 +235,5 @@ impl Options for Settings { } } -let a = Settings::default().parse(std::env::args_os()).a; +let a = Settings::default().parse(std::env::args_os()).0.a; ``` diff --git a/docs/guide/quick.md b/docs/guide/quick.md index 5f5a12d..2a150b9 100644 --- a/docs/guide/quick.md +++ b/docs/guide/quick.md @@ -13,8 +13,11 @@ We can create arguments by annotating a variant of an `enum` deriving [`Argument To represent the program configuration we create a struct called `Settings`, which implements `Options`. When an argument is encountered, we _apply_ it to the `Settings` struct. In this case, we set the `force` field of `Settings` to `true` if `Arg::Force` is parsed. +Any arguments that are not flags are returned as well as part of the tuple returned by `parse`. These do not have special treatment in this library. + ```rust use uutils_args::{Arguments, Options}; +use std::ffi::OsString; #[derive(Arguments)] enum Arg { @@ -35,8 +38,16 @@ impl Options for Settings { } } -assert!(!Settings::default().parse(["test"]).force); -assert!(Settings::default().parse(["test", "-f"]).force); +let (settings, operands) = Settings::default().parse(["test"]); +assert!(!settings.force); +assert_eq!(operands, Vec::::new()); + +let (settings, operands) = Settings::default().parse(["test", "-f"]); +assert!(settings.force); + +let (settings, operands) = Settings::default().parse(["test", "foo"]); +assert!(!settings.force); +assert_eq!(operands, vec![OsString::from("foo")]); ``` ## Two overriding flags @@ -45,6 +56,7 @@ Of course, we can define multiple flags. If these arguments change the same fiel ```rust use uutils_args::{Arguments, Options}; +use std::ffi::OsString; #[derive(Arguments)] enum Arg { @@ -68,9 +80,18 @@ impl Options for Settings { } } -assert!(!Settings::default().parse(["test"]).force); -assert!(Settings::default().parse(["test", "-f"]).force); -assert!(!Settings::default().parse(["test", "-f", "-F"]).force); +let (settings, operands) = Settings::default().parse(["test"]); +assert!(!settings.force); +assert_eq!(operands, Vec::::new()); + +let (settings, operands) = Settings::default().parse(["test", "-f", "some-operand"]); +assert!(settings.force); + +assert_eq!(operands, vec!["some-operand"]); +let (settings, operands) = Settings::default().parse(["test", "-f", "-F", "some-other-operand"]); +assert!(!settings.force); + +assert_eq!(operands, vec!["some-other-operand"]); ``` ## Help strings @@ -120,11 +141,11 @@ enum Arg { # } # # assert_eq!( -# Settings::default().parse(["test"]).name, +# Settings::default().parse(["test"]).0.name, # OsString::new(), # ); # assert_eq!( -# Settings::default().parse(["test", "--name=John"]).name, +# Settings::default().parse(["test", "--name=John"]).0.name, # OsString::from("John"), # ); ``` @@ -157,11 +178,11 @@ enum Arg { # } # # assert_eq!( -# Settings::default().parse(["test", "--name"]).name, +# Settings::default().parse(["test", "--name"]).0.name, # OsString::from("anonymous"), # ); # assert_eq!( -# Settings::default().parse(["test", "--name=John"]).name, +# Settings::default().parse(["test", "--name=John"]).0.name, # OsString::from("John"), # ); ``` @@ -193,9 +214,9 @@ enum Arg { # } # } # -# assert!(!Settings::default().parse(["test"]).force); -# assert!(Settings::default().parse(["test", "-f"]).force); -# assert!(!Settings::default().parse(["test", "-F"]).force); +# assert!(!Settings::default().parse(["test"]).0.force); +# assert!(Settings::default().parse(["test", "-f"]).0.force); +# assert!(!Settings::default().parse(["test", "-F"]).0.force); ``` This is particularly interesting for defining "shortcut" arguments. For example, `ls` takes a `--sort=WORD` argument, that defines how the files should be sorted. But it also has shorthands like `-t`, which is the same as `--sort=time`. All of these can be implemented on one variant: @@ -228,48 +249,7 @@ enum Arg { # } # } # -# assert_eq!(Settings::default().parse(["test"]).sort, String::new()); -# assert_eq!(Settings::default().parse(["test", "--sort=time"]).sort, String::from("time")); -# assert_eq!(Settings::default().parse(["test", "-t"]).sort, String::from("time")); +# assert_eq!(Settings::default().parse(["test"]).0.sort, String::new()); +# assert_eq!(Settings::default().parse(["test", "--sort=time"]).0.sort, String::from("time")); +# assert_eq!(Settings::default().parse(["test", "-t"]).0.sort, String::from("time")); ``` - -## Positional arguments - -Finally, at the end of this whirlwind tour, we get to positional arguments. Here's a simple positional argument: - -```rust -use uutils_args::{Arguments, Options}; -use std::path::PathBuf; - -#[derive(Arguments)] -enum Arg { - #[arg("FILES", ..)] - File(PathBuf) -} - -#[derive(Default, Debug, PartialEq, Eq)] -struct Settings { - files: Vec, -} - -impl Options for Settings { - fn apply(&mut self, arg: Arg) { - match arg { - Arg::File(f) => self.files.push(f), - } - } -} -# -# assert_eq!( -# Settings::default().parse(["test"]).files, -# Vec::::new(), -# ); -# assert_eq!( -# Settings::default().parse(["test", "foo"]).files, -# vec![PathBuf::from("foo")], -# ); -# assert_eq!( -# Settings::default().parse(["test", "foo", "bar"]).files, -# vec![PathBuf::from("foo"), PathBuf::from("bar")], -# ); -``` \ No newline at end of file From 88019ffe4b4257b0f3043aabb2516265384cedda Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Tue, 19 Dec 2023 13:22:39 +0100 Subject: [PATCH 048/116] add docs for `Value` trait and derive --- derive/src/lib.rs | 20 ++++------------ docs/guide/value.md | 56 +++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 35 +++++++++++++++++++++++++++- 3 files changed, 95 insertions(+), 16 deletions(-) diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 5a74dfe..486530c 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -1,6 +1,9 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +//! Derive macros for `uutils_args`. All items here are documented in that +//! crate. + mod argument; mod attributes; mod complete; @@ -18,21 +21,7 @@ use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, Data::Enum, DeriveInput}; -/// Derive `Arguments` -/// -/// ## Argument specifications -/// -/// | specification | kind | value | -/// | -------------- | ---------- | -------- | -/// | `VAL` | positional | n.a. | -/// | `-s` | short | none | -/// | `-s VAL` | short | required | -/// | `-s[VAL]` | short | optional | -/// | `--long` | long | none | -/// | `--long=VAL` | long | required | -/// | `--long[=VAL]` | long | optional | -/// | `long=VAL` | dd | required | -/// +/// Documentation for this can be found in `uutils_args`. #[proc_macro_derive(Arguments, attributes(arg, arguments))] pub fn arguments(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); @@ -125,6 +114,7 @@ pub fn arguments(input: TokenStream) -> TokenStream { TokenStream::from(expanded) } +/// Documentation for this can be found in `uutils_args`. #[proc_macro_derive(Value, attributes(value))] pub fn value(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); diff --git a/docs/guide/value.md b/docs/guide/value.md index 4c689c8..7cbff6e 100644 --- a/docs/guide/value.md +++ b/docs/guide/value.md @@ -1 +1,57 @@ # Value trait + +Any field on the enum implementing [`Arguments`](trait@crate::Arguments) has to implement the [`Value`](trait@crate::Value) trait, which determines how it is derive from the text value. Normally, [`Value`](trait@crate::Value) only requires one method: [`from_value`](crate::Value::from_value), which takes an `&OsStr` and returns a `Result` with either `Self` or some boxed error. + +This trait is implemented for common types, such as integers, [`OsString`](std::ffi::OsString), [`PathBuf`](std::path::PathBuf), [`String`] and [`Option`] where `T` implements `Value`. + +There is also a [`Value` derive macro](derive@crate::Value), which provides parsing string values into an `enum`. The name of each variant (lowercased) with a `#[value]` attribute is parsed automatically. Additionally, if the string is an unambiguous prefix, it is also parsed. For example, if we have the values `"yes"` and `"no"` then `"y"`, `"ye"`, `"yes"` are all valid for `"yes"`, because no other values start with those substrings. + +```rust +use uutils_args::Value; +use std::ffi::OsStr; + +#[derive(Value, Debug, PartialEq, Eq)] +enum YesOrNo { + #[value] + Yes, + #[value] + No, +} + +assert_eq!(YesOrNo::from_value(OsStr::new("yes")).unwrap(), YesOrNo::Yes); +assert_eq!(YesOrNo::from_value(OsStr::new("no")).unwrap(), YesOrNo::No); +assert_eq!(YesOrNo::from_value(OsStr::new("y")).unwrap(), YesOrNo::Yes); +assert_eq!(YesOrNo::from_value(OsStr::new("n")).unwrap(), YesOrNo::No); +assert!(YesOrNo::from_value(OsStr::new("YES")).is_err()); +assert!(YesOrNo::from_value(OsStr::new("NO")).is_err()); +assert!(YesOrNo::from_value(OsStr::new("maybe")).is_err()); +``` + +We can also provide custom names for the variants. This is useful if there are multiple strings that should parse to one variant. + +```rust +use uutils_args::Value; +use std::ffi::OsStr; + +#[derive(Value, Debug, PartialEq, Eq)] +enum Color { + #[value("yes", "always")] + Always, + #[value("auto")] + Auto, + #[value("no", "never")] + Never, +} + +assert_eq!(Color::from_value(&OsStr::new("yes")).unwrap(), Color::Always); +assert_eq!(Color::from_value(&OsStr::new("always")).unwrap(), Color::Always); +assert_eq!(Color::from_value(&OsStr::new("auto")).unwrap(), Color::Auto); +assert_eq!(Color::from_value(&OsStr::new("no")).unwrap(), Color::Never); +assert_eq!(Color::from_value(&OsStr::new("never")).unwrap(), Color::Never); + +// The prefixes here are interesting: +// - "a" is ambiguous because it is a prefix of "auto" and "always" +// - "n" is not ambiguous because "no" and "never" map to the same variant +assert!(Color::from_value(&OsStr::new("a")).is_err()); +assert_eq!(Color::from_value(&OsStr::new("n")).unwrap(), Color::Never); +``` diff --git a/src/lib.rs b/src/lib.rs index 87716a1..a7afc2d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,7 +12,40 @@ mod value; pub mod docs; pub use lexopt; -pub use uutils_args_derive::*; + +// The documentation for the derive macros is written here instead of in +// `uutils_args_derive`, because we need to be able to link to items and the +// documentation in this crate. + +/// Derive macro for [`Value`](trait@crate::Value) +/// +/// [See also the chapter on this trait in the guide](crate::docs::guide::value) +/// +/// This macro only works on `enums` and will error at compile time when it is +/// used on a `struct`. +pub use uutils_args_derive::Value; + +/// Derive macro for [`Arguments`](trait@crate::Arguments) +/// +/// [See also the chapter on this trait in the guide](crate::docs::guide::quick) +/// +/// This macro only works on `enums` and will error at compile time when it is +/// used on a `struct`. +/// +/// /// ## Argument specifications +/// +/// | specification | kind | value | +/// | -------------- | ---------- | -------- | +/// | `VAL` | positional | n.a. | +/// | `-s` | short | none | +/// | `-s VAL` | short | required | +/// | `-s[VAL]` | short | optional | +/// | `--long` | long | none | +/// | `--long=VAL` | long | required | +/// | `--long[=VAL]` | long | optional | +/// | `long=VAL` | dd | required | +/// +pub use uutils_args_derive::Arguments; pub use error::{Error, ErrorKind}; pub use value::{Value, ValueError, ValueResult}; From f01d94d8dae8bbba222f009f7609b4aa0d437fce Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Tue, 19 Dec 2023 14:39:22 +0100 Subject: [PATCH 049/116] add navigation links to docs --- docs/guide/completions.md | 35 +++++++++++++++++++++++++++++++++++ docs/guide/port.md | 35 +++++++++++++++++++++++++++++++++++ docs/guide/quick.md | 36 ++++++++++++++++++++++++++++++++++++ docs/guide/value.md | 35 +++++++++++++++++++++++++++++++++++ src/docs.rs | 26 ++++++++++++++++++-------- src/lib.rs | 2 +- 6 files changed, 160 insertions(+), 9 deletions(-) diff --git a/docs/guide/completions.md b/docs/guide/completions.md index efecf40..f325b91 100644 --- a/docs/guide/completions.md +++ b/docs/guide/completions.md @@ -1,3 +1,30 @@ + +
+ +[Previous](previous) +[Up](super) +[Next]() + +
+ # Completions Shell completions and documentation can be generated automatically by this crate. The implementation for this is in the [`uutils-args-complete`] crate. The easiest way of generating completions is via the `parse-is-complete` feature flag. This feature flag hijacks the [`Options::parse`](crate::Options::parse) function to print completions. This means that there is usually no need to write any additional code to generate completions. @@ -13,3 +40,11 @@ The `[shell]` value here can be `fish`, `zsh`, `bash`, `powershell`, `elvish` or 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. + +
+ +[Previous](previous) +[Up](super) +[Next]() + +
\ No newline at end of file diff --git a/docs/guide/port.md b/docs/guide/port.md index a154a10..2cc2981 100644 --- a/docs/guide/port.md +++ b/docs/guide/port.md @@ -1,3 +1,30 @@ + +
+ +[Previous](previous) +[Up](super) +[Next](next) + +
+ # Porting from Clap This chapter contains information about how common patterns in `clap` parsers can be ported to `uutils-args`. @@ -237,3 +264,11 @@ impl Options for Settings { let a = Settings::default().parse(std::env::args_os()).0.a; ``` + +
+ +[Previous](previous) +[Up](super) +[Next](next) + +
\ No newline at end of file diff --git a/docs/guide/quick.md b/docs/guide/quick.md index 2a150b9..6b928ea 100644 --- a/docs/guide/quick.md +++ b/docs/guide/quick.md @@ -1,3 +1,31 @@ + +
+ +[Previous]() +[Up](super) +[Next](next) + +
+ # Quick Start A parser consists of two parts: @@ -253,3 +281,11 @@ enum Arg { # assert_eq!(Settings::default().parse(["test", "--sort=time"]).0.sort, String::from("time")); # assert_eq!(Settings::default().parse(["test", "-t"]).0.sort, String::from("time")); ``` + +
+ +[Previous]() +[Up](super) +[Next](next) + +
\ No newline at end of file diff --git a/docs/guide/value.md b/docs/guide/value.md index 7cbff6e..443a960 100644 --- a/docs/guide/value.md +++ b/docs/guide/value.md @@ -1,3 +1,30 @@ + +
+ +[Previous](previous) +[Up](super) +[Next](next) + +
+ # Value trait Any field on the enum implementing [`Arguments`](trait@crate::Arguments) has to implement the [`Value`](trait@crate::Value) trait, which determines how it is derive from the text value. Normally, [`Value`](trait@crate::Value) only requires one method: [`from_value`](crate::Value::from_value), which takes an `&OsStr` and returns a `Result` with either `Self` or some boxed error. @@ -55,3 +82,11 @@ assert_eq!(Color::from_value(&OsStr::new("never")).unwrap(), Color::Never); assert!(Color::from_value(&OsStr::new("a")).is_err()); assert_eq!(Color::from_value(&OsStr::new("n")).unwrap(), Color::Never); ``` + +
+ +[Previous](previous) +[Up](super) +[Next](next) + +
\ No newline at end of file diff --git a/src/docs.rs b/src/docs.rs index 3168ade..045ef04 100644 --- a/src/docs.rs +++ b/src/docs.rs @@ -5,14 +5,24 @@ #[doc = include_str!("../docs/guide/guide.md")] pub mod guide { - #[doc = include_str!("../docs/guide/quick.md")] - pub mod quick {} - #[doc = include_str!("../docs/guide/port.md")] - pub mod port {} - #[doc = include_str!("../docs/guide/completions.md")] - pub mod completions {} - #[doc = include_str!("../docs/guide/value.md")] - pub mod value {} + pub mod quick { + #![doc = include_str!("../docs/guide/quick.md")] + pub use super::port as next; + } + pub mod port { + #![doc = include_str!("../docs/guide/port.md")] + pub use super::quick as previous; + pub use super::value as next; + } + pub mod value { + #![doc = include_str!("../docs/guide/value.md")] + pub use super::completions as next; + pub use super::port as previous; + } + pub mod completions { + #![doc = include_str!("../docs/guide/completions.md")] + pub use super::value as previous; + } } #[doc = include_str!("../docs/design/design.md")] diff --git a/src/lib.rs b/src/lib.rs index a7afc2d..9d0a4e8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,7 +31,7 @@ pub use uutils_args_derive::Value; /// /// This macro only works on `enums` and will error at compile time when it is /// used on a `struct`. -/// +/// /// /// ## Argument specifications /// /// | specification | kind | value | From 49ede1997c707f15b90021eaf4103ab0782bc030 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Tue, 19 Dec 2023 14:47:09 +0100 Subject: [PATCH 050/116] make link to guide big --- src/lib.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 9d0a4e8..8e76960 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,12 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -//! [Click here to check out the guide-level documentation](docs::guide) +//!
+//! +//! [Click here for the guide](docs::guide) +//! +//!
+//! #![doc = include_str!("../README.md")] mod error; From 43f792f91e15e832cdc01c5d6b8f5351c2d8c333 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Tue, 19 Dec 2023 15:13:12 +0100 Subject: [PATCH 051/116] remove parse and rename try_parse to parse --- README.md | 2 +- docs/guide/port.md | 10 +-- docs/guide/quick.md | 35 ++++---- examples/completion.rs | 2 +- examples/deprecated.rs | 17 +++- examples/hello_world.rs | 3 +- examples/value.rs | 2 +- src/lib.rs | 37 +------- tests/coreutils/arch.rs | 6 +- tests/coreutils/b2sum.rs | 52 +++++++++-- tests/coreutils/base32.rs | 22 ++++- tests/coreutils/basename.rs | 2 +- tests/coreutils/cat.rs | 16 ++-- tests/coreutils/dd.rs | 23 +++-- tests/coreutils/echo.rs | 6 +- tests/coreutils/head.rs | 2 +- tests/coreutils/ls.rs | 54 ++++++----- tests/coreutils/mktemp.rs | 22 ++--- tests/coreutils/tail.rs | 2 +- tests/flags.rs | 131 +++++++++++++++++++-------- tests/options.rs | 173 ++++++++++++++++++++++++++++-------- 21 files changed, 414 insertions(+), 205 deletions(-) diff --git a/README.md b/README.md index 839a510..d27b71e 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ impl Options for Settings { } fn run(args: &[&str]) -> String { - let (s, operands) = Settings::default().parse(args); + let (s, operands) = Settings::default().parse(args).unwrap(); let text = operands.iter().map(|s| s.to_string_lossy()).collect::>().join(" "); let mut output = if s.caps { text.to_uppercase() diff --git a/docs/guide/port.md b/docs/guide/port.md index 2cc2981..f7d6070 100644 --- a/docs/guide/port.md +++ b/docs/guide/port.md @@ -101,7 +101,7 @@ impl Options for Settings { } } -let a = Settings::default().parse(std::env::args_os()).0.a; +let a = Settings::default().parse(std::env::args_os()).unwrap().0.a; ``` ### `ArgAction::SetFalse` @@ -144,7 +144,7 @@ impl Options for Settings { } } -let a = Settings::default().parse(std::env::args_os()).0.a; +let a = Settings::default().parse(std::env::args_os()).unwrap().0.a; ``` ### `ArgAction::Count` @@ -182,7 +182,7 @@ impl Options for Settings { } } -let a = Settings::default().parse(std::env::args_os()).0.a; +let a = Settings::default().parse(std::env::args_os()).unwrap().0.a; ``` ### `ArgAction::Set` @@ -222,7 +222,7 @@ impl Options for Settings { } } -let a = Settings::default().parse(std::env::args_os()).0.a; +let a = Settings::default().parse(std::env::args_os()).unwrap().0.a; ``` ### `ArgAction::Append` @@ -262,7 +262,7 @@ impl Options for Settings { } } -let a = Settings::default().parse(std::env::args_os()).0.a; +let a = Settings::default().parse(std::env::args_os()).unwrap().0.a; ```
diff --git a/docs/guide/quick.md b/docs/guide/quick.md index 6b928ea..31f515b 100644 --- a/docs/guide/quick.md +++ b/docs/guide/quick.md @@ -66,14 +66,14 @@ impl Options for Settings { } } -let (settings, operands) = Settings::default().parse(["test"]); +let (settings, operands) = Settings::default().parse(["test"]).unwrap(); assert!(!settings.force); assert_eq!(operands, Vec::::new()); -let (settings, operands) = Settings::default().parse(["test", "-f"]); +let (settings, operands) = Settings::default().parse(["test", "-f"]).unwrap(); assert!(settings.force); -let (settings, operands) = Settings::default().parse(["test", "foo"]); +let (settings, operands) = Settings::default().parse(["test", "foo"]).unwrap(); assert!(!settings.force); assert_eq!(operands, vec![OsString::from("foo")]); ``` @@ -108,17 +108,16 @@ impl Options for Settings { } } -let (settings, operands) = Settings::default().parse(["test"]); +let (settings, operands) = Settings::default().parse(["test"]).unwrap(); assert!(!settings.force); assert_eq!(operands, Vec::::new()); -let (settings, operands) = Settings::default().parse(["test", "-f", "some-operand"]); +let (settings, operands) = Settings::default().parse(["test", "-f", "some-operand"]).unwrap(); assert!(settings.force); - assert_eq!(operands, vec!["some-operand"]); -let (settings, operands) = Settings::default().parse(["test", "-f", "-F", "some-other-operand"]); -assert!(!settings.force); +let (settings, operands) = Settings::default().parse(["test", "-f", "-F", "some-other-operand"]).unwrap(); +assert!(!settings.force); assert_eq!(operands, vec!["some-other-operand"]); ``` @@ -169,11 +168,11 @@ enum Arg { # } # # assert_eq!( -# Settings::default().parse(["test"]).0.name, +# Settings::default().parse(["test"]).unwrap().0.name, # OsString::new(), # ); # assert_eq!( -# Settings::default().parse(["test", "--name=John"]).0.name, +# Settings::default().parse(["test", "--name=John"]).unwrap().0.name, # OsString::from("John"), # ); ``` @@ -206,11 +205,11 @@ enum Arg { # } # # assert_eq!( -# Settings::default().parse(["test", "--name"]).0.name, +# Settings::default().parse(["test", "--name"]).unwrap().0.name, # OsString::from("anonymous"), # ); # assert_eq!( -# Settings::default().parse(["test", "--name=John"]).0.name, +# Settings::default().parse(["test", "--name=John"]).unwrap().0.name, # OsString::from("John"), # ); ``` @@ -242,9 +241,9 @@ enum Arg { # } # } # -# assert!(!Settings::default().parse(["test"]).0.force); -# assert!(Settings::default().parse(["test", "-f"]).0.force); -# assert!(!Settings::default().parse(["test", "-F"]).0.force); +# assert!(!Settings::default().parse(["test"]).unwrap().0.force); +# assert!(Settings::default().parse(["test", "-f"]).unwrap().0.force); +# assert!(!Settings::default().parse(["test", "-F"]).unwrap().0.force); ``` This is particularly interesting for defining "shortcut" arguments. For example, `ls` takes a `--sort=WORD` argument, that defines how the files should be sorted. But it also has shorthands like `-t`, which is the same as `--sort=time`. All of these can be implemented on one variant: @@ -277,9 +276,9 @@ enum Arg { # } # } # -# assert_eq!(Settings::default().parse(["test"]).0.sort, String::new()); -# assert_eq!(Settings::default().parse(["test", "--sort=time"]).0.sort, String::from("time")); -# assert_eq!(Settings::default().parse(["test", "-t"]).0.sort, String::from("time")); +# assert_eq!(Settings::default().parse(["test"]).unwrap().0.sort, String::new()); +# assert_eq!(Settings::default().parse(["test", "--sort=time"]).unwrap().0.sort, String::from("time")); +# assert_eq!(Settings::default().parse(["test", "-t"]).unwrap().0.sort, String::from("time")); ```
diff --git a/examples/completion.rs b/examples/completion.rs index 22679c6..112cc60 100644 --- a/examples/completion.rs +++ b/examples/completion.rs @@ -38,5 +38,5 @@ impl Options for Settings { } fn main() { - Settings.parse(std::env::args_os()); + Settings.parse(std::env::args_os()).unwrap(); } diff --git a/examples/deprecated.rs b/examples/deprecated.rs index 25484b6..2a6d5bd 100644 --- a/examples/deprecated.rs +++ b/examples/deprecated.rs @@ -43,8 +43,17 @@ impl Options for Settings { } fn main() { - assert_eq!(Settings::default().parse(["test", "-10"]).0.n1, 10usize); - assert!(Settings::default().try_parse(["test", "--10"]).is_err()); - assert_eq!(Settings::default().parse(["test", "+10"]).0.n2, 10isize); - assert_eq!(Settings::default().parse(["test", "+-10"]).0.n2, -10isize); + assert_eq!( + Settings::default().parse(["test", "-10"]).unwrap().0.n1, + 10usize + ); + assert!(Settings::default().parse(["test", "--10"]).is_err()); + assert_eq!( + Settings::default().parse(["test", "+10"]).unwrap().0.n2, + 10isize + ); + assert_eq!( + Settings::default().parse(["test", "+-10"]).unwrap().0.n2, + -10isize + ); } diff --git a/examples/hello_world.rs b/examples/hello_world.rs index a8eb551..7bd4eb0 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -36,7 +36,8 @@ fn main() -> Result<(), uutils_args::Error> { name: String::new(), count: 1, } - .parse(std::env::args_os()); + .parse(std::env::args_os()) + .unwrap(); for _ in 0..settings.count { println!("Hello, {}!", settings.name); diff --git a/examples/value.rs b/examples/value.rs index 997d136..5359192 100644 --- a/examples/value.rs +++ b/examples/value.rs @@ -33,6 +33,6 @@ impl Options for Settings { } fn main() { - let (settings, _operands) = Settings::default().parse(std::env::args_os()); + let (settings, _operands) = Settings::default().parse(std::env::args_os()).unwrap(); println!("{:?}", settings.color); } diff --git a/src/lib.rs b/src/lib.rs index 8e76960..ac971cd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,16 +67,6 @@ pub enum Argument { Custom(T), } -fn exit_if_err(res: Result) -> T { - match res { - Ok(v) => v, - Err(err) => { - eprintln!("{err}"); - std::process::exit(err.exit_code); - } - } -} - /// Defines how the arguments are parsed. /// /// If a type `T` implements this trait, we can construct an `ArgumentIter`, @@ -112,23 +102,11 @@ pub trait Arguments: Sized { /// Get the version string for this command. fn version() -> String; - /// Check all arguments immediately and exit on errors. - /// - /// This is useful if you want to validate the arguments. This method will - /// exit if `--help` or `--version` are passed and if any errors are found. - fn check(args: I) - where - I: IntoIterator, - I::Item: Into, - { - exit_if_err(Self::try_check(args)) - } - /// Check all arguments immediately and return any errors. /// /// This is useful if you want to validate the arguments. This method will /// exit if `--help` or `--version` are passed. - fn try_check(args: I) -> Result<(), Error> + fn check(args: I) -> Result<(), Error> where I: IntoIterator, I::Item: Into, @@ -145,8 +123,7 @@ pub trait Arguments: Sized { /// An iterator over arguments. /// /// Can be constructed by calling [`Arguments::parse`]. Usually, this method -/// won't be used directly, but is used internally in [`Options::parse`] and -/// [`Options::try_parse`]. +/// won't be used directly, but is used internally in [`Options::parse`]. pub struct ArgumentIter { parser: lexopt::Parser, positional_arguments: Vec, @@ -218,16 +195,8 @@ pub trait Options: Sized { fn apply(&mut self, arg: Arg); /// Parse an iterator of arguments into the options - fn parse(self, args: I) -> (Self, Vec) - where - I: IntoIterator, - I::Item: Into, - { - exit_if_err(self.try_parse(args)) - } - #[allow(unused_mut)] - fn try_parse(mut self, args: I) -> Result<(Self, Vec), Error> + fn parse(mut self, args: I) -> Result<(Self, Vec), Error> where I: IntoIterator, I::Item: Into, diff --git a/tests/coreutils/arch.rs b/tests/coreutils/arch.rs index e69b639..266505d 100644 --- a/tests/coreutils/arch.rs +++ b/tests/coreutils/arch.rs @@ -5,11 +5,11 @@ enum Arg {} #[test] fn no_args() { - assert!(Arg::try_check(["arch"]).is_ok()); + assert!(Arg::check(["arch"]).is_ok()); } #[test] fn one_arg_fails() { - assert!(Arg::try_check(["arch", "-f"]).is_err()); - assert!(Arg::try_check(["arch", "--foo"]).is_err()); + assert!(Arg::check(["arch", "-f"]).is_err()); + assert!(Arg::check(["arch", "--foo"]).is_err()); } diff --git a/tests/coreutils/b2sum.rs b/tests/coreutils/b2sum.rs index adae103..3f24c71 100644 --- a/tests/coreutils/b2sum.rs +++ b/tests/coreutils/b2sum.rs @@ -62,26 +62,52 @@ impl Options for Settings { #[test] fn binary() { - assert!(!Settings::default().parse(["b2sum"]).0.binary); - assert!(!Settings::default().parse(["b2sum", "--text"]).0.binary); - assert!(!Settings::default().parse(["b2sum", "-t"]).0.binary); + assert!(!Settings::default().parse(["b2sum"]).unwrap().0.binary); + assert!( + !Settings::default() + .parse(["b2sum", "--text"]) + .unwrap() + .0 + .binary + ); + assert!(!Settings::default().parse(["b2sum", "-t"]).unwrap().0.binary); assert!( !Settings::default() .parse(["b2sum", "--binary", "--text"]) + .unwrap() + .0 + .binary + ); + assert!( + !Settings::default() + .parse(["b2sum", "-b", "-t"]) + .unwrap() .0 .binary ); - assert!(!Settings::default().parse(["b2sum", "-b", "-t"]).0.binary); - assert!(Settings::default().parse(["b2sum", "--binary"]).0.binary); - assert!(Settings::default().parse(["b2sum", "-b"]).0.binary); + assert!( + Settings::default() + .parse(["b2sum", "--binary"]) + .unwrap() + .0 + .binary + ); + assert!(Settings::default().parse(["b2sum", "-b"]).unwrap().0.binary); assert!( Settings::default() .parse(["b2sum", "--text", "--binary"]) + .unwrap() + .0 + .binary + ); + assert!( + Settings::default() + .parse(["b2sum", "-t", "-b"]) + .unwrap() .0 .binary ); - assert!(Settings::default().parse(["b2sum", "-t", "-b"]).0.binary); } #[test] @@ -89,6 +115,7 @@ fn check_output() { assert_eq!( Settings::default() .parse(["b2sum", "--warn"]) + .unwrap() .0 .check_output, CheckOutput::Warn @@ -96,6 +123,7 @@ fn check_output() { assert_eq!( Settings::default() .parse(["b2sum", "--quiet"]) + .unwrap() .0 .check_output, CheckOutput::Quiet @@ -103,6 +131,7 @@ fn check_output() { assert_eq!( Settings::default() .parse(["b2sum", "--status"]) + .unwrap() .0 .check_output, CheckOutput::Status @@ -110,6 +139,7 @@ fn check_output() { assert_eq!( Settings::default() .parse(["b2sum", "--status", "--warn"]) + .unwrap() .0 .check_output, CheckOutput::Warn @@ -117,6 +147,7 @@ fn check_output() { assert_eq!( Settings::default() .parse(["b2sum", "--status", "--warn"]) + .unwrap() .0 .check_output, CheckOutput::Warn @@ -125,6 +156,7 @@ fn check_output() { assert_eq!( Settings::default() .parse(["b2sum", "--warn", "--quiet"]) + .unwrap() .0 .check_output, CheckOutput::Quiet @@ -133,6 +165,7 @@ fn check_output() { assert_eq!( Settings::default() .parse(["b2sum", "--quiet", "--status"]) + .unwrap() .0 .check_output, CheckOutput::Status @@ -142,7 +175,10 @@ fn check_output() { #[test] fn files() { assert_eq!( - Settings::default().parse(["b2sum", "foo", "bar"]).1, + Settings::default() + .parse(["b2sum", "foo", "bar"]) + .unwrap() + .1, vec![OsString::from("foo"), OsString::from("bar")] ); } diff --git a/tests/coreutils/base32.rs b/tests/coreutils/base32.rs index be54c57..de32e74 100644 --- a/tests/coreutils/base32.rs +++ b/tests/coreutils/base32.rs @@ -41,14 +41,28 @@ impl Options for Settings { #[test] fn wrap() { - assert_eq!(Settings::default().parse(["base32"]).0.wrap, Some(76)); - assert_eq!(Settings::default().parse(["base32", "-w0"]).0.wrap, None); assert_eq!( - Settings::default().parse(["base32", "-w100"]).0.wrap, + Settings::default().parse(["base32"]).unwrap().0.wrap, + Some(76) + ); + assert_eq!( + Settings::default().parse(["base32", "-w0"]).unwrap().0.wrap, + None + ); + assert_eq!( + Settings::default() + .parse(["base32", "-w100"]) + .unwrap() + .0 + .wrap, Some(100) ); assert_eq!( - Settings::default().parse(["base32", "--wrap=100"]).0.wrap, + Settings::default() + .parse(["base32", "--wrap=100"]) + .unwrap() + .0 + .wrap, Some(100) ); } diff --git a/tests/coreutils/basename.rs b/tests/coreutils/basename.rs index accb8ce..7f0716e 100644 --- a/tests/coreutils/basename.rs +++ b/tests/coreutils/basename.rs @@ -36,7 +36,7 @@ impl Options for Settings { } fn parse(args: &[&str]) -> Settings { - let (mut settings, operands) = Settings::default().parse(args); + let (mut settings, operands) = Settings::default().parse(args).unwrap(); settings.names = operands; if !settings.multiple { assert_eq!(settings.names.len(), 2); diff --git a/tests/coreutils/cat.rs b/tests/coreutils/cat.rs index 190d6c1..03df331 100644 --- a/tests/coreutils/cat.rs +++ b/tests/coreutils/cat.rs @@ -75,27 +75,27 @@ impl Options for Settings { #[test] fn show() { - let (s, _) = Settings::default().parse(["cat", "-v"]); + let (s, _) = Settings::default().parse(["cat", "-v"]).unwrap(); assert!(!s.show_ends && !s.show_tabs && s.show_nonprinting); - let (s, _) = Settings::default().parse(["cat", "-E"]); + let (s, _) = Settings::default().parse(["cat", "-E"]).unwrap(); assert!(s.show_ends && !s.show_tabs && !s.show_nonprinting); - let (s, _) = Settings::default().parse(["cat", "-T"]); + let (s, _) = Settings::default().parse(["cat", "-T"]).unwrap(); assert!(!s.show_ends && s.show_tabs && !s.show_nonprinting); - let (s, _) = Settings::default().parse(["cat", "-e"]); + let (s, _) = Settings::default().parse(["cat", "-e"]).unwrap(); assert!(s.show_ends && !s.show_tabs && s.show_nonprinting); - let (s, _) = Settings::default().parse(["cat", "-t"]); + let (s, _) = Settings::default().parse(["cat", "-t"]).unwrap(); assert!(!s.show_ends && s.show_tabs && s.show_nonprinting); - let (s, _) = Settings::default().parse(["cat", "-A"]); + let (s, _) = Settings::default().parse(["cat", "-A"]).unwrap(); assert!(s.show_ends && s.show_tabs && s.show_nonprinting); - let (s, _) = Settings::default().parse(["cat", "-te"]); + let (s, _) = Settings::default().parse(["cat", "-te"]).unwrap(); assert!(s.show_ends && s.show_tabs && s.show_nonprinting); - let (s, _) = Settings::default().parse(["cat", "-vET"]); + let (s, _) = Settings::default().parse(["cat", "-vET"]).unwrap(); assert!(s.show_ends && s.show_tabs && s.show_nonprinting); } diff --git a/tests/coreutils/dd.rs b/tests/coreutils/dd.rs index 20ae14c..fe5ef69 100644 --- a/tests/coreutils/dd.rs +++ b/tests/coreutils/dd.rs @@ -116,13 +116,16 @@ impl Options for Settings { #[test] fn empty() { - assert_eq!(Settings::default().parse(["dd"]).0, Settings::default()) + assert_eq!( + Settings::default().parse(["dd"]).unwrap().0, + Settings::default() + ) } #[test] fn infile() { assert_eq!( - Settings::default().parse(["dd", "if=hello"]).0, + Settings::default().parse(["dd", "if=hello"]).unwrap().0, Settings { infile: Some(PathBuf::from("hello")), ..Settings::default() @@ -133,7 +136,7 @@ fn infile() { #[test] fn outfile() { assert_eq!( - Settings::default().parse(["dd", "of=hello"]).0, + Settings::default().parse(["dd", "of=hello"]).unwrap().0, Settings { outfile: Some(PathBuf::from("hello")), ..Settings::default() @@ -144,7 +147,7 @@ fn outfile() { #[test] fn bs() { assert_eq!( - Settings::default().parse(["dd", "ibs=1"]).0, + Settings::default().parse(["dd", "ibs=1"]).unwrap().0, Settings { ibs: 1, obs: 512, @@ -152,7 +155,7 @@ fn bs() { } ); assert_eq!( - Settings::default().parse(["dd", "obs=1"]).0, + Settings::default().parse(["dd", "obs=1"]).unwrap().0, Settings { ibs: 512, obs: 1, @@ -160,7 +163,10 @@ fn bs() { } ); assert_eq!( - Settings::default().parse(["dd", "ibs=10", "obs=1"]).0, + Settings::default() + .parse(["dd", "ibs=10", "obs=1"]) + .unwrap() + .0, Settings { ibs: 10, obs: 1, @@ -168,7 +174,10 @@ fn bs() { } ); assert_eq!( - Settings::default().parse(["dd", "ibs=10", "bs=1"]).0, + Settings::default() + .parse(["dd", "ibs=10", "bs=1"]) + .unwrap() + .0, Settings { ibs: 1, obs: 1, diff --git a/tests/coreutils/echo.rs b/tests/coreutils/echo.rs index 4016c46..2d561f2 100644 --- a/tests/coreutils/echo.rs +++ b/tests/coreutils/echo.rs @@ -39,16 +39,16 @@ impl Options for Settings { #[test] #[ignore = "needs to be fixed after positional argument refactor"] fn double_hyphen() { - let (_, operands) = Settings::default().parse(["echo", "--"]); + let (_, operands) = Settings::default().parse(["echo", "--"]).unwrap(); assert_eq!(operands, vec![OsString::from("--")]); - let (_, operands) = Settings::default().parse(["echo", "--", "-n"]); + let (_, operands) = Settings::default().parse(["echo", "--", "-n"]).unwrap(); assert_eq!(operands, vec![OsString::from("--"), OsString::from("-n")]); } #[test] #[ignore] fn nonexistent_options_are_values() { - let (_, operands) = Settings::default().parse(["echo", "-f"]); + let (_, operands) = Settings::default().parse(["echo", "-f"]).unwrap(); assert_eq!(operands, vec![OsString::from("-f")]); } diff --git a/tests/coreutils/head.rs b/tests/coreutils/head.rs index ab08a9e..cdf8326 100644 --- a/tests/coreutils/head.rs +++ b/tests/coreutils/head.rs @@ -212,7 +212,7 @@ where { match parse_deprecated(iter.clone()) { Some(s) => Ok(s), - None => Settings::default().try_parse(iter), + None => Settings::default().parse(iter), } } diff --git a/tests/coreutils/ls.rs b/tests/coreutils/ls.rs index 1a23c92..681e83c 100644 --- a/tests/coreutils/ls.rs +++ b/tests/coreutils/ls.rs @@ -423,7 +423,7 @@ impl Options for Settings { #[test] fn default() { assert_eq!( - Settings::default().parse(["ls"]).0, + Settings::default().parse(["ls"]).unwrap().0, Settings { format: Format::Columns, sort: Sort::Name, @@ -454,87 +454,95 @@ fn default() { #[test] fn color() { - let (s, _operands) = Settings::default().parse(["ls", "--color"]); + let (s, _operands) = Settings::default().parse(["ls", "--color"]).unwrap(); assert!(s.color); - let (s, _operands) = Settings::default().parse(["ls", "--color=always"]); + let (s, _operands) = Settings::default().parse(["ls", "--color=always"]).unwrap(); assert!(s.color); - let (s, _operands) = Settings::default().parse(["ls", "--color=never"]); + let (s, _operands) = Settings::default().parse(["ls", "--color=never"]).unwrap(); assert!(!s.color); } #[test] fn format() { - let (s, _operands) = Settings::default().parse(["ls", "-l"]); + let (s, _operands) = Settings::default().parse(["ls", "-l"]).unwrap(); assert_eq!(s.format, Format::Long); - let (s, _operands) = Settings::default().parse(["ls", "-m"]); + let (s, _operands) = Settings::default().parse(["ls", "-m"]).unwrap(); assert_eq!(s.format, Format::Commas); - let (s, _operands) = Settings::default().parse(["ls", "--format=across"]); + let (s, _operands) = Settings::default() + .parse(["ls", "--format=across"]) + .unwrap(); assert_eq!(s.format, Format::Across); - let (s, _operands) = Settings::default().parse(["ls", "--format=acr"]); + let (s, _operands) = Settings::default().parse(["ls", "--format=acr"]).unwrap(); assert_eq!(s.format, Format::Across); - let (s, _operands) = Settings::default().parse(["ls", "-o"]); + let (s, _operands) = Settings::default().parse(["ls", "-o"]).unwrap(); assert_eq!(s.format, Format::Long); assert!(s.long_no_group && !s.long_no_owner && !s.long_numeric_uid_gid); - let (s, _operands) = Settings::default().parse(["ls", "-g"]); + let (s, _operands) = Settings::default().parse(["ls", "-g"]).unwrap(); assert_eq!(s.format, Format::Long); assert!(!s.long_no_group && s.long_no_owner && !s.long_numeric_uid_gid); - let (s, _operands) = Settings::default().parse(["ls", "-n"]); + let (s, _operands) = Settings::default().parse(["ls", "-n"]).unwrap(); assert_eq!(s.format, Format::Long); assert!(!s.long_no_group && !s.long_no_owner && s.long_numeric_uid_gid); - let (s, _operands) = Settings::default().parse(["ls", "-og"]); + let (s, _operands) = Settings::default().parse(["ls", "-og"]).unwrap(); assert_eq!(s.format, Format::Long); assert!(s.long_no_group && s.long_no_owner && !s.long_numeric_uid_gid); - let (s, _operands) = Settings::default().parse(["ls", "-on"]); + let (s, _operands) = Settings::default().parse(["ls", "-on"]).unwrap(); assert_eq!(s.format, Format::Long); assert!(s.long_no_group && !s.long_no_owner && s.long_numeric_uid_gid); - let (s, _operands) = Settings::default().parse(["ls", "-onCl"]); + let (s, _operands) = Settings::default().parse(["ls", "-onCl"]).unwrap(); assert_eq!(s.format, Format::Long); assert!(s.long_no_group && !s.long_no_owner && s.long_numeric_uid_gid); } #[test] fn time() { - let (s, _operands) = Settings::default().parse(["ls", "--time=access"]); + let (s, _operands) = Settings::default().parse(["ls", "--time=access"]).unwrap(); assert_eq!(s.time, Time::Access); - let (s, _operands) = Settings::default().parse(["ls", "--time=a"]); + let (s, _operands) = Settings::default().parse(["ls", "--time=a"]).unwrap(); assert_eq!(s.time, Time::Access); } #[test] fn classify() { - let (s, _operands) = Settings::default().parse(["ls", "--indicator-style=classify"]); + let (s, _operands) = Settings::default() + .parse(["ls", "--indicator-style=classify"]) + .unwrap(); assert_eq!(s.indicator_style, IndicatorStyle::Classify); - let (s, _operands) = Settings::default().parse(["ls", "--classify"]); + let (s, _operands) = Settings::default().parse(["ls", "--classify"]).unwrap(); assert_eq!(s.indicator_style, IndicatorStyle::Classify); - let (s, _operands) = Settings::default().parse(["ls", "--classify=always"]); + let (s, _operands) = Settings::default() + .parse(["ls", "--classify=always"]) + .unwrap(); assert_eq!(s.indicator_style, IndicatorStyle::Classify); - let (s, _operands) = Settings::default().parse(["ls", "--classify=none"]); + let (s, _operands) = Settings::default() + .parse(["ls", "--classify=none"]) + .unwrap(); assert_eq!(s.indicator_style, IndicatorStyle::None); - let (s, _operands) = Settings::default().parse(["ls", "-F"]); + let (s, _operands) = Settings::default().parse(["ls", "-F"]).unwrap(); assert_eq!(s.indicator_style, IndicatorStyle::Classify); } #[test] fn sort() { - let (s, _operands) = Settings::default().parse(["ls", "--sort=time"]); + let (s, _operands) = Settings::default().parse(["ls", "--sort=time"]).unwrap(); assert_eq!(s.sort, Sort::Time); - let (s, _operands) = Settings::default().parse(["ls", "-X"]); + let (s, _operands) = Settings::default().parse(["ls", "-X"]).unwrap(); assert_eq!(s.sort, Sort::Extension); } diff --git a/tests/coreutils/mktemp.rs b/tests/coreutils/mktemp.rs index c90d019..1319709 100644 --- a/tests/coreutils/mktemp.rs +++ b/tests/coreutils/mktemp.rs @@ -48,35 +48,37 @@ impl Options for Settings { #[test] fn suffix() { - let (s, _operands) = Settings::default().parse(["mktemp", "--suffix=hello"]); + let (s, _operands) = Settings::default() + .parse(["mktemp", "--suffix=hello"]) + .unwrap(); assert_eq!(s.suffix.unwrap(), "hello"); - let (s, _operands) = Settings::default().parse(["mktemp", "--suffix="]); + let (s, _operands) = Settings::default().parse(["mktemp", "--suffix="]).unwrap(); assert_eq!(s.suffix.unwrap(), ""); - let (s, _operands) = Settings::default().parse(["mktemp", "--suffix="]); + let (s, _operands) = Settings::default().parse(["mktemp", "--suffix="]).unwrap(); assert_eq!(s.suffix.unwrap(), ""); - let (s, _operands) = Settings::default().parse(["mktemp"]); + let (s, _operands) = Settings::default().parse(["mktemp"]).unwrap(); assert_eq!(s.suffix, None); } #[test] fn tmpdir() { - let (s, _operands) = Settings::default().parse(["mktemp", "--tmpdir"]); + let (s, _operands) = Settings::default().parse(["mktemp", "--tmpdir"]).unwrap(); assert_eq!(s.tmp_dir.unwrap(), Path::new(".")); - let (s, _operands) = Settings::default().parse(["mktemp", "--tmpdir="]); + let (s, _operands) = Settings::default().parse(["mktemp", "--tmpdir="]).unwrap(); assert_eq!(s.tmp_dir.unwrap(), Path::new("")); - let (s, _operands) = Settings::default().parse(["mktemp", "-p", "foo"]); + let (s, _operands) = Settings::default().parse(["mktemp", "-p", "foo"]).unwrap(); assert_eq!(s.tmp_dir.unwrap(), Path::new("foo")); - let (s, _operands) = Settings::default().parse(["mktemp", "-pfoo"]); + let (s, _operands) = Settings::default().parse(["mktemp", "-pfoo"]).unwrap(); assert_eq!(s.tmp_dir.unwrap(), Path::new("foo")); - let (s, _operands) = Settings::default().parse(["mktemp", "-p", ""]); + let (s, _operands) = Settings::default().parse(["mktemp", "-p", ""]).unwrap(); assert_eq!(s.tmp_dir.unwrap(), Path::new("")); - assert!(Settings::default().try_parse(["mktemp", "-p"]).is_err()); + assert!(Settings::default().parse(["mktemp", "-p"]).is_err()); } diff --git a/tests/coreutils/tail.rs b/tests/coreutils/tail.rs index f8f75d2..2c9573e 100644 --- a/tests/coreutils/tail.rs +++ b/tests/coreutils/tail.rs @@ -278,7 +278,7 @@ where { match parse_deprecated(iter.clone()) { Some(s) => Ok(s), - None => Settings::default().try_parse(iter), + None => Settings::default().parse(iter), } } diff --git a/tests/flags.rs b/tests/flags.rs index c1960cf..c9877d1 100644 --- a/tests/flags.rs +++ b/tests/flags.rs @@ -21,7 +21,7 @@ fn one_flag() { } } - let (settings, _) = Settings::default().parse(["test", "-f"]); + let (settings, _) = Settings::default().parse(["test", "-f"]).unwrap(); assert!(settings.foo); } @@ -51,19 +51,19 @@ fn two_flags() { } assert_eq!( - Settings::default().parse(["test", "-a"]).0, + Settings::default().parse(["test", "-a"]).unwrap().0, Settings { a: true, b: false } ); assert_eq!( - Settings::default().parse(["test"]).0, + Settings::default().parse(["test"]).unwrap().0, Settings { a: false, b: false } ); assert_eq!( - Settings::default().parse(["test", "-b"]).0, + Settings::default().parse(["test", "-b"]).unwrap().0, Settings { a: false, b: true } ); assert_eq!( - Settings::default().parse(["test", "-a", "-b"]).0, + Settings::default().parse(["test", "-a", "-b"]).unwrap().0, Settings { a: true, b: true } ); } @@ -87,9 +87,9 @@ fn long_and_short_flag() { } } - assert!(!Settings::default().parse(["test"]).0.foo); - assert!(Settings::default().parse(["test", "--foo"]).0.foo); - assert!(Settings::default().parse(["test", "-f"]).0.foo); + assert!(!Settings::default().parse(["test"]).unwrap().0.foo); + assert!(Settings::default().parse(["test", "--foo"]).unwrap().0.foo); + assert!(Settings::default().parse(["test", "-f"]).unwrap().0.foo); } #[test] @@ -111,7 +111,7 @@ fn short_alias() { } } - assert!(Settings::default().parse(["test", "-b"]).0.foo); + assert!(Settings::default().parse(["test", "-b"]).unwrap().0.foo); } #[test] @@ -133,7 +133,7 @@ fn long_alias() { } } - assert!(Settings::default().parse(["test", "--bar"]).0.foo); + assert!(Settings::default().parse(["test", "--bar"]).unwrap().0.foo); } #[test] @@ -171,10 +171,22 @@ fn short_and_long_alias() { bar: true, }; - assert_eq!(Settings::default().parse(["test", "--bar"]).0, foo_true); - assert_eq!(Settings::default().parse(["test", "-b"]).0, foo_true); - assert_eq!(Settings::default().parse(["test", "--foo"]).0, bar_true); - assert_eq!(Settings::default().parse(["test", "-f"]).0, bar_true); + assert_eq!( + Settings::default().parse(["test", "--bar"]).unwrap().0, + foo_true + ); + assert_eq!( + Settings::default().parse(["test", "-b"]).unwrap().0, + foo_true + ); + assert_eq!( + Settings::default().parse(["test", "--foo"]).unwrap().0, + bar_true + ); + assert_eq!( + Settings::default().parse(["test", "-f"]).unwrap().0, + bar_true + ); } #[test] @@ -217,7 +229,7 @@ fn xyz_map_to_abc() { } assert_eq!( - Settings::default().parse(["test", "-x"]).0, + Settings::default().parse(["test", "-x"]).unwrap().0, Settings { a: true, b: true, @@ -226,7 +238,7 @@ fn xyz_map_to_abc() { ); assert_eq!( - Settings::default().parse(["test", "-y"]).0, + Settings::default().parse(["test", "-y"]).unwrap().0, Settings { a: false, b: true, @@ -235,7 +247,7 @@ fn xyz_map_to_abc() { ); assert_eq!( - Settings::default().parse(["test", "-xy"]).0, + Settings::default().parse(["test", "-xy"]).unwrap().0, Settings { a: true, b: true, @@ -244,7 +256,7 @@ fn xyz_map_to_abc() { ); assert_eq!( - Settings::default().parse(["test", "-z"]).0, + Settings::default().parse(["test", "-z"]).unwrap().0, Settings { a: true, b: true, @@ -281,6 +293,7 @@ fn non_rust_ident() { assert_eq!( Settings::default() .parse(["test", "--foo-bar", "--super"]) + .unwrap() .0, Settings { a: true, b: true } ) @@ -304,7 +317,7 @@ fn number_flag() { } } - assert!(Settings::default().parse(["test", "-1"]).0.one) + assert!(Settings::default().parse(["test", "-1"]).unwrap().0.one) } #[test] @@ -331,12 +344,24 @@ fn false_bool() { } } - assert!(Settings::default().parse(["test", "-a"]).0.foo); - assert!(!Settings::default().parse(["test", "-b"]).0.foo); - assert!(!Settings::default().parse(["test", "-ab"]).0.foo); - assert!(Settings::default().parse(["test", "-ba"]).0.foo); - assert!(!Settings::default().parse(["test", "-a", "-b"]).0.foo); - assert!(Settings::default().parse(["test", "-b", "-a"]).0.foo); + assert!(Settings::default().parse(["test", "-a"]).unwrap().0.foo); + assert!(!Settings::default().parse(["test", "-b"]).unwrap().0.foo); + assert!(!Settings::default().parse(["test", "-ab"]).unwrap().0.foo); + assert!(Settings::default().parse(["test", "-ba"]).unwrap().0.foo); + assert!( + !Settings::default() + .parse(["test", "-a", "-b"]) + .unwrap() + .0 + .foo + ); + assert!( + Settings::default() + .parse(["test", "-b", "-a"]) + .unwrap() + .0 + .foo + ); } #[test] @@ -358,9 +383,30 @@ fn verbosity() { } } - assert_eq!(Settings::default().parse(["test", "-v"]).0.verbosity, 1); - assert_eq!(Settings::default().parse(["test", "-vv"]).0.verbosity, 2); - assert_eq!(Settings::default().parse(["test", "-vvv"]).0.verbosity, 3); + assert_eq!( + Settings::default() + .parse(["test", "-v"]) + .unwrap() + .0 + .verbosity, + 1 + ); + assert_eq!( + Settings::default() + .parse(["test", "-vv"]) + .unwrap() + .0 + .verbosity, + 2 + ); + assert_eq!( + Settings::default() + .parse(["test", "-vvv"]) + .unwrap() + .0 + .verbosity, + 3 + ); } #[test] @@ -392,10 +438,22 @@ fn infer_long_args() { } } - assert!(Settings::default().parse(["test", "--all"]).0.all); - assert!(Settings::default().parse(["test", "--alm"]).0.almost_all); - assert!(Settings::default().parse(["test", "--au"]).0.author); - assert!(Settings::default().try_parse(["test", "--a"]).is_err()); + assert!(Settings::default().parse(["test", "--all"]).unwrap().0.all); + assert!( + Settings::default() + .parse(["test", "--alm"]) + .unwrap() + .0 + .almost_all + ); + assert!( + Settings::default() + .parse(["test", "--au"]) + .unwrap() + .0 + .author + ); + assert!(Settings::default().parse(["test", "--a"]).is_err()); } #[test] @@ -433,13 +491,16 @@ fn enum_flag() { } } - assert_eq!(Settings::default().parse(["test"]).0.foo, SomeEnum::Foo); assert_eq!( - Settings::default().parse(["test", "--bar"]).0.foo, + Settings::default().parse(["test"]).unwrap().0.foo, + SomeEnum::Foo + ); + assert_eq!( + Settings::default().parse(["test", "--bar"]).unwrap().0.foo, SomeEnum::Bar ); assert_eq!( - Settings::default().parse(["test", "--baz"]).0.foo, + Settings::default().parse(["test", "--baz"]).unwrap().0.foo, SomeEnum::Baz, ); } diff --git a/tests/options.rs b/tests/options.rs index 4a328d5..2a41f27 100644 --- a/tests/options.rs +++ b/tests/options.rs @@ -24,6 +24,7 @@ fn string_option() { assert_eq!( Settings::default() .parse(["test", "--message=hello"]) + .unwrap() .0 .message, "hello" @@ -61,13 +62,18 @@ fn enum_option() { } assert_eq!( - Settings::default().parse(["test", "--format=bar"]).0.format, + Settings::default() + .parse(["test", "--format=bar"]) + .unwrap() + .0 + .format, Format::Bar ); assert_eq!( Settings::default() .parse(["test", "--format", "baz"]) + .unwrap() .0 .format, Format::Baz @@ -103,11 +109,19 @@ fn enum_option_with_fields() { } assert_eq!( - Settings::default().parse(["test", "-i=thin"]).0.indent, + Settings::default() + .parse(["test", "-i=thin"]) + .unwrap() + .0 + .indent, Indent::Spaces(4) ); assert_eq!( - Settings::default().parse(["test", "-i=wide"]).0.indent, + Settings::default() + .parse(["test", "-i=wide"]) + .unwrap() + .0 + .indent, Indent::Spaces(8) ); } @@ -152,11 +166,19 @@ fn enum_with_complex_from_value() { } assert_eq!( - Settings::default().parse(["test", "-i=tabs"]).0.indent, + Settings::default() + .parse(["test", "-i=tabs"]) + .unwrap() + .0 + .indent, Indent::Tabs ); assert_eq!( - Settings::default().parse(["test", "-i=4"]).0.indent, + Settings::default() + .parse(["test", "-i=4"]) + .unwrap() + .0 + .indent, Indent::Spaces(4) ); } @@ -192,30 +214,51 @@ fn color() { } assert_eq!( - Settings::default().parse(["test", "--color=yes"]).0.color, + Settings::default() + .parse(["test", "--color=yes"]) + .unwrap() + .0 + .color, Color::Always ); assert_eq!( Settings::default() .parse(["test", "--color=always"]) + .unwrap() .0 .color, Color::Always ); assert_eq!( - Settings::default().parse(["test", "--color=no"]).0.color, + Settings::default() + .parse(["test", "--color=no"]) + .unwrap() + .0 + .color, Color::Never ); assert_eq!( - Settings::default().parse(["test", "--color=never"]).0.color, + Settings::default() + .parse(["test", "--color=never"]) + .unwrap() + .0 + .color, Color::Never ); assert_eq!( - Settings::default().parse(["test", "--color=auto"]).0.color, + Settings::default() + .parse(["test", "--color=auto"]) + .unwrap() + .0 + .color, Color::Auto ); assert_eq!( - Settings::default().parse(["test", "--color"]).0.color, + Settings::default() + .parse(["test", "--color"]) + .unwrap() + .0 + .color, Color::Always ) } @@ -252,8 +295,9 @@ fn actions() { } } - let (settings, _operands) = - Settings::default().parse(["test", "-m=Hello", "-m=World", "--send"]); + let (settings, _operands) = Settings::default() + .parse(["test", "-m=Hello", "-m=World", "--send"]) + .unwrap(); assert_eq!(settings.messages, vec!["Hello", "World"]); assert_eq!(settings.last_message, "World"); assert!(settings.send); @@ -281,8 +325,14 @@ fn width() { } } - assert_eq!(Settings::default().parse(["test", "-w=0"]).0.width, None); - assert_eq!(Settings::default().parse(["test", "-w=1"]).0.width, Some(1)); + assert_eq!( + Settings::default().parse(["test", "-w=0"]).unwrap().0.width, + None + ); + assert_eq!( + Settings::default().parse(["test", "-w=1"]).unwrap().0.width, + Some(1) + ); } #[test] @@ -333,17 +383,47 @@ fn integers() { } } - assert_eq!(Settings::default().parse(["test", "--u8=5"]).0.n, 5); - assert_eq!(Settings::default().parse(["test", "--u16=5"]).0.n, 5); - assert_eq!(Settings::default().parse(["test", "--u32=5"]).0.n, 5); - assert_eq!(Settings::default().parse(["test", "--u64=5"]).0.n, 5); - assert_eq!(Settings::default().parse(["test", "--u128=5"]).0.n, 5); + assert_eq!( + Settings::default().parse(["test", "--u8=5"]).unwrap().0.n, + 5 + ); + assert_eq!( + Settings::default().parse(["test", "--u16=5"]).unwrap().0.n, + 5 + ); + assert_eq!( + Settings::default().parse(["test", "--u32=5"]).unwrap().0.n, + 5 + ); + assert_eq!( + Settings::default().parse(["test", "--u64=5"]).unwrap().0.n, + 5 + ); + assert_eq!( + Settings::default().parse(["test", "--u128=5"]).unwrap().0.n, + 5 + ); - assert_eq!(Settings::default().parse(["test", "--i8=5"]).0.n, 5); - assert_eq!(Settings::default().parse(["test", "--i16=5"]).0.n, 5); - assert_eq!(Settings::default().parse(["test", "--i32=5"]).0.n, 5); - assert_eq!(Settings::default().parse(["test", "--i64=5"]).0.n, 5); - assert_eq!(Settings::default().parse(["test", "--i128=5"]).0.n, 5); + assert_eq!( + Settings::default().parse(["test", "--i8=5"]).unwrap().0.n, + 5 + ); + assert_eq!( + Settings::default().parse(["test", "--i16=5"]).unwrap().0.n, + 5 + ); + assert_eq!( + Settings::default().parse(["test", "--i32=5"]).unwrap().0.n, + 5 + ); + assert_eq!( + Settings::default().parse(["test", "--i64=5"]).unwrap().0.n, + 5 + ); + assert_eq!( + Settings::default().parse(["test", "--i128=5"]).unwrap().0.n, + 5 + ); } #[test] @@ -379,23 +459,35 @@ fn ls_classify() { } } - assert_eq!(Settings::default().parse(["test"]).0.classify, When::Auto); + assert_eq!( + Settings::default().parse(["test"]).unwrap().0.classify, + When::Auto + ); assert_eq!( Settings::default() .parse(["test", "--classify=never"]) + .unwrap() .0 .classify, When::Never, ); assert_eq!( - Settings::default().parse(["test", "--classify"]).0.classify, + Settings::default() + .parse(["test", "--classify"]) + .unwrap() + .0 + .classify, When::Always, ); assert_eq!( - Settings::default().parse(["test", "-F"]).0.classify, + Settings::default() + .parse(["test", "-F"]) + .unwrap() + .0 + .classify, When::Always, ); - assert!(Settings::default().try_parse(["test", "-Falways"]).is_err()); + assert!(Settings::default().parse(["test", "-Falways"]).is_err()); } #[test] @@ -420,16 +512,16 @@ fn mktemp_tmpdir() { } } - let (settings, _operands) = Settings::default().parse(["test", "-p", "X"]); + let (settings, _operands) = Settings::default().parse(["test", "-p", "X"]).unwrap(); assert_eq!(settings.tmpdir.unwrap(), "X"); - let (settings, _operands) = Settings::default().parse(["test", "--tmpdir=X"]); + let (settings, _operands) = Settings::default().parse(["test", "--tmpdir=X"]).unwrap(); assert_eq!(settings.tmpdir.unwrap(), "X"); - let (settings, _operands) = Settings::default().parse(["test", "--tmpdir"]); + let (settings, _operands) = Settings::default().parse(["test", "--tmpdir"]).unwrap(); assert_eq!(settings.tmpdir.unwrap(), "/tmp"); - assert!(Settings::default().try_parse(["test", "-p"]).is_err()); + assert!(Settings::default().parse(["test", "-p"]).is_err()); } #[test] @@ -497,8 +589,17 @@ fn deprecated() { } } - assert_eq!(Settings::default().parse(["test", "-10"]).0.n1, 10usize); - assert!(Settings::default().try_parse(["test", "--10"]).is_err()); - assert_eq!(Settings::default().parse(["test", "+10"]).0.n2, 10isize); - assert_eq!(Settings::default().parse(["test", "+-10"]).0.n2, -10isize); + assert_eq!( + Settings::default().parse(["test", "-10"]).unwrap().0.n1, + 10usize + ); + assert!(Settings::default().parse(["test", "--10"]).is_err()); + assert_eq!( + Settings::default().parse(["test", "+10"]).unwrap().0.n2, + 10isize + ); + assert_eq!( + Settings::default().parse(["test", "+-10"]).unwrap().0.n2, + -10isize + ); } From c206234fa02618cf9f1b3848ba789a6ff51f01c9 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Tue, 19 Dec 2023 15:21:56 +0100 Subject: [PATCH 052/116] simplify external API --- src/lib.rs | 47 ++++++++--------------------------------------- 1 file changed, 8 insertions(+), 39 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index ac971cd..780f7ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -69,9 +69,6 @@ pub enum Argument { /// Defines how the arguments are parsed. /// -/// If a type `T` implements this trait, we can construct an `ArgumentIter`, -/// meaning that we can parse the individual arguments to `T`.\ -/// /// Usually, this trait will be implemented via the /// [derive macro](derive@Arguments) and does not need to be implemented /// manually. @@ -79,19 +76,7 @@ pub trait Arguments: Sized { /// The exit code to exit the program with on error. const EXIT_CODE: i32; - /// Parse an iterator of arguments into an - /// [`ArgumentIter`](ArgumentIter). - fn parse(args: I) -> ArgumentIter - where - I: IntoIterator, - I::Item: Into, - { - ArgumentIter::::from_args(args) - } - /// Parse the next argument from the lexopt parser. - /// - /// This method is called by [`ArgumentIter::next_arg`]. fn next_arg(parser: &mut lexopt::Parser) -> Result>, ErrorKind>; /// Print the help string for this command. @@ -111,7 +96,7 @@ pub trait Arguments: Sized { I: IntoIterator, I::Item: Into, { - let mut iter = Self::parse(args); + let mut iter = ArgumentIter::::from_args(args); while iter.next_arg()?.is_some() {} Ok(()) } @@ -121,10 +106,7 @@ pub trait Arguments: Sized { } /// An iterator over arguments. -/// -/// Can be constructed by calling [`Arguments::parse`]. Usually, this method -/// won't be used directly, but is used internally in [`Options::parse`]. -pub struct ArgumentIter { +struct ArgumentIter { parser: lexopt::Parser, positional_arguments: Vec, t: PhantomData, @@ -150,11 +132,11 @@ impl ArgumentIter { })? { match arg { Argument::Help => { - self.help().unwrap(); + T::help(self.parser.bin_name().unwrap()).unwrap(); std::process::exit(0); } Argument::Version => { - print!("{}", self.version()); + print!("{}", T::version()); std::process::exit(0); } Argument::Positional(arg) => { @@ -165,18 +147,6 @@ impl ArgumentIter { } Ok(None) } - - fn get_positional_arguments(self) -> Vec { - self.positional_arguments - } - - fn help(&self) -> std::io::Result<()> { - T::help(self.parser.bin_name().unwrap()) - } - - fn version(&self) -> String { - T::version() - } } /// Defines the app settings by consuming [`Arguments`]. @@ -187,9 +157,8 @@ impl ArgumentIter { /// - the [`apply`](Options::apply) method, which defines to how map that /// type onto the options. /// -/// By default, the [`Options::parse`] method will -/// 1. repeatedly call [`ArgumentIter::next_arg`] and call [`Options::apply`] -/// on the result until the arguments are exhausted. +/// By default, the [`Options::parse`] method iterate over the arguments and +/// call [`Options::apply`] on the result until the arguments are exhausted. pub trait Options: Sized { /// Apply a single argument to the options. fn apply(&mut self, arg: Arg); @@ -215,11 +184,11 @@ pub trait Options: Sized { #[cfg(not(feature = "parse-is-complete"))] { - let mut iter = Arg::parse(args); + let mut iter = ArgumentIter::::from_args(args); while let Some(arg) = iter.next_arg()? { self.apply(arg); } - Ok((self, iter.get_positional_arguments())) + Ok((self, iter.positional_arguments)) } } From b3ee70b84cbb4ea74f8e9fc9165bafc2b30325f3 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Tue, 19 Dec 2023 15:29:20 +0100 Subject: [PATCH 053/116] make help() return a String --- derive/src/help.rs | 16 ++++++++-------- derive/src/lib.rs | 2 +- src/internal.rs | 17 ++++++++--------- src/lib.rs | 4 ++-- 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/derive/src/help.rs b/derive/src/help.rs index a606ecf..77af149 100644 --- a/derive/src/help.rs +++ b/derive/src/help.rs @@ -74,27 +74,27 @@ pub fn help_string( } let options = if !options.is_empty() { - quote!(::uutils_args::internal::print_flags(&mut w, #indent, #width, [#(#options),*])?;) + quote!(::uutils_args::internal::print_flags(&mut w, #indent, #width, [#(#options),*]);) } else { quote!() }; quote!( - let mut w = ::std::io::stdout(); - use ::std::io::Write; + let mut w = String::new(); + use ::std::fmt::Write; writeln!(w, "{} {}", option_env!("CARGO_BIN_NAME").unwrap_or(env!("CARGO_PKG_NAME")), env!("CARGO_PKG_VERSION"), - )?; + ).unwrap(); - writeln!(w, "{}", #summary)?; + writeln!(w, "{}", #summary).unwrap(); - writeln!(w, "\nUsage:\n {}", format!(#usage, bin_name))?; + writeln!(w, "\nUsage:\n {}", format!(#usage, bin_name)).unwrap(); #options - writeln!(w, "{}", #after_options)?; - Ok(()) + writeln!(w, "{}", #after_options).unwrap(); + w ) } diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 486530c..45b8df5 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -95,7 +95,7 @@ pub fn arguments(input: TokenStream) -> TokenStream { } } - fn help(bin_name: &str) -> ::std::io::Result<()> { + fn help(bin_name: &str) -> String { #help_string } diff --git a/src/internal.rs b/src/internal.rs index 412c737..b74dedd 100644 --- a/src/internal.rs +++ b/src/internal.rs @@ -14,7 +14,7 @@ use crate::error::ErrorKind; use crate::value::Value; use std::{ ffi::{OsStr, OsString}, - io::Write, + fmt::Write, }; /// Parses an echo-style positional argument @@ -118,33 +118,32 @@ pub fn print_flags( indent_size: usize, width: usize, options: impl IntoIterator, -) -> std::io::Result<()> { +) { let indent = " ".repeat(indent_size); - writeln!(w, "\nOptions:")?; + writeln!(w, "\nOptions:").unwrap(); for (flags, help_string) in options { let mut help_lines = help_string.lines(); - write!(w, "{}{}", &indent, &flags)?; + write!(w, "{}{}", &indent, &flags).unwrap(); if flags.len() <= width { let line = match help_lines.next() { Some(line) => line, None => { - writeln!(w)?; + writeln!(w).unwrap(); continue; } }; let help_indent = " ".repeat(width - flags.len() + 2); - writeln!(w, "{}{}", help_indent, line)?; + writeln!(w, "{}{}", help_indent, line).unwrap(); } else { - writeln!(w)?; + writeln!(w).unwrap(); } let help_indent = " ".repeat(width + indent_size + 2); for line in help_lines { - writeln!(w, "{}{}", help_indent, line)?; + writeln!(w, "{}{}", help_indent, line).unwrap(); } } - Ok(()) } #[cfg(test)] diff --git a/src/lib.rs b/src/lib.rs index 780f7ac..694bcdf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -82,7 +82,7 @@ pub trait Arguments: Sized { /// Print the help string for this command. /// /// The `bin_name` specifies the name that executable was called with. - fn help(bin_name: &str) -> std::io::Result<()>; + fn help(bin_name: &str) -> String; /// Get the version string for this command. fn version() -> String; @@ -132,7 +132,7 @@ impl ArgumentIter { })? { match arg { Argument::Help => { - T::help(self.parser.bin_name().unwrap()).unwrap(); + print!("{}", T::help(self.parser.bin_name().unwrap())); std::process::exit(0); } Argument::Version => { From 922979135d815888a519c9a99151d18e3963c64c Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Tue, 19 Dec 2023 16:34:05 +0100 Subject: [PATCH 054/116] make value derive not fail without #[value] --- derive/src/lib.rs | 11 +++++++---- tests/options.rs | 24 ++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 45b8df5..3b2fbdd 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -150,16 +150,18 @@ pub fn value(input: TokenStream) -> TokenStream { options.push(quote!(&[#(#keys),*])); let stmt = if let Some(v) = value { - quote!(#(| #keys)* => #v) + quote!(#(| #keys)* => #v,) } else { let mut v = variant.clone(); v.attrs = vec![]; - quote!(#(| #keys)* => Self::#v) + quote!(#(| #keys)* => Self::#v,) }; match_arms.push(stmt); } } + let keys_len = all_keys.len(); + let expanded = quote!( impl #impl_generics Value for #name #ty_generics #where_clause { fn from_value(value: &::std::ffi::OsStr) -> ::uutils_args::ValueResult { @@ -191,15 +193,16 @@ pub fn value(input: TokenStream) -> TokenStream { }; Ok(match opt { - #(#match_arms),*, + #(#match_arms)* _ => unreachable!("Should be caught by (None, []) case above.") }) } #[cfg(feature = "complete")] fn value_hint() -> ::uutils_args_complete::ValueHint { + let keys: [&str; #keys_len] = [#(#all_keys),*]; ::uutils_args_complete::ValueHint::Strings( - [#(#all_keys),*] + keys .into_iter() .map(ToString::to_string) .collect() diff --git a/tests/options.rs b/tests/options.rs index 2a41f27..1f1e972 100644 --- a/tests/options.rs +++ b/tests/options.rs @@ -603,3 +603,27 @@ fn deprecated() { -10isize ); } + +#[test] +#[allow(unreachable_code)] +fn empty_value() { + // We just check that this compiles + #[derive(Value)] + enum V {} + + #[derive(Arguments)] + enum Arg { + #[arg("--val=VAL")] + Val(V), + } + + struct Settings {} + + impl Options for Settings { + fn apply(&mut self, arg: Arg) { + match arg { + Arg::Val(_) => {} + } + } + } +} From 338f4c60258d9be553a1d6100ff86399cac11957 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 21 Dec 2023 00:55:42 +0000 Subject: [PATCH 055/116] Update Rust crate syn to 2.0.42 --- derive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 3f3bc83..e1200ad 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -11,4 +11,4 @@ proc_macro = true [dependencies] proc-macro2 = "1.0.70" quote = "1.0.33" -syn = { version = "2.0.41", features = ["full"] } +syn = { version = "2.0.42", features = ["full"] } From 83621891e383e4bc800d81153fac0d853c06c5ff Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 21 Dec 2023 19:48:25 +0000 Subject: [PATCH 056/116] Update Rust crate proc-macro2 to 1.0.71 --- derive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index e1200ad..0669dfa 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -9,6 +9,6 @@ edition = "2021" proc_macro = true [dependencies] -proc-macro2 = "1.0.70" +proc-macro2 = "1.0.71" quote = "1.0.33" syn = { version = "2.0.42", features = ["full"] } From 373c14ee8704267c1a66420b0f6e8cf96ea835d5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 25 Dec 2023 06:55:42 +0000 Subject: [PATCH 057/116] Update Rust crate syn to 2.0.43 --- derive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 0669dfa..d5d73fa 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -11,4 +11,4 @@ proc_macro = true [dependencies] proc-macro2 = "1.0.71" quote = "1.0.33" -syn = { version = "2.0.42", features = ["full"] } +syn = { version = "2.0.43", features = ["full"] } From 859354e909e3707f6a37f5cf5691a81a189d6af3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 31 Dec 2023 03:03:15 +0000 Subject: [PATCH 058/116] Update Rust crate proc-macro2 to 1.0.72 --- derive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index d5d73fa..c181297 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -9,6 +9,6 @@ edition = "2021" proc_macro = true [dependencies] -proc-macro2 = "1.0.71" +proc-macro2 = "1.0.72" quote = "1.0.33" syn = { version = "2.0.43", features = ["full"] } From b3b0f7aa203d77fb970dcf4b85ed3ce175b0325c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 1 Jan 2024 01:51:43 +0000 Subject: [PATCH 059/116] Update Rust crate proc-macro2 to 1.0.73 --- derive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index c181297..6abcad7 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -9,6 +9,6 @@ edition = "2021" proc_macro = true [dependencies] -proc-macro2 = "1.0.72" +proc-macro2 = "1.0.73" quote = "1.0.33" syn = { version = "2.0.43", features = ["full"] } From 6308bb46ecb3d04a7759db20eafc95043897a6d4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 1 Jan 2024 04:23:19 +0000 Subject: [PATCH 060/116] Update Rust crate syn to 2.0.44 --- derive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index c181297..d2208eb 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -11,4 +11,4 @@ proc_macro = true [dependencies] proc-macro2 = "1.0.72" quote = "1.0.33" -syn = { version = "2.0.43", features = ["full"] } +syn = { version = "2.0.44", features = ["full"] } From 481a28ae70921e82a3858e7274e516bbdb4000b0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 1 Jan 2024 06:13:33 +0000 Subject: [PATCH 061/116] Update Rust crate quote to 1.0.34 --- derive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index f2052e4..ca62634 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -10,5 +10,5 @@ proc_macro = true [dependencies] proc-macro2 = "1.0.73" -quote = "1.0.33" +quote = "1.0.34" syn = { version = "2.0.44", features = ["full"] } From 3390c76df9f02bb4f1367c7368b74689c66026f3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 1 Jan 2024 21:02:40 +0000 Subject: [PATCH 062/116] Update Rust crate syn to 2.0.45 --- derive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index ca62634..c7320b2 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -11,4 +11,4 @@ proc_macro = true [dependencies] proc-macro2 = "1.0.73" quote = "1.0.34" -syn = { version = "2.0.44", features = ["full"] } +syn = { version = "2.0.45", features = ["full"] } From 5d7c50d404b1ef38f3edf2e560014518a5172257 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Jan 2024 05:56:23 +0000 Subject: [PATCH 063/116] Update Rust crate proc-macro2 to 1.0.74 --- derive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index c7320b2..f4da9ec 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -9,6 +9,6 @@ edition = "2021" proc_macro = true [dependencies] -proc-macro2 = "1.0.73" +proc-macro2 = "1.0.74" quote = "1.0.34" syn = { version = "2.0.45", features = ["full"] } From ad6859f4ccd6740051457c9be35b55e6adec97ed Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Jan 2024 11:11:32 +0000 Subject: [PATCH 064/116] Update Rust crate quote to 1.0.35 --- derive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index f4da9ec..9d2fed1 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -10,5 +10,5 @@ proc_macro = true [dependencies] proc-macro2 = "1.0.74" -quote = "1.0.34" +quote = "1.0.35" syn = { version = "2.0.45", features = ["full"] } From 7afc634e86930a7d6541091ef9258aa68087e6ef Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Jan 2024 12:34:53 +0000 Subject: [PATCH 065/116] Update Rust crate syn to 2.0.46 --- derive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 9d2fed1..41a00d0 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -11,4 +11,4 @@ proc_macro = true [dependencies] proc-macro2 = "1.0.74" quote = "1.0.35" -syn = { version = "2.0.45", features = ["full"] } +syn = { version = "2.0.46", features = ["full"] } From 058d53dec70ce72d6136ae2c66261f7f01d5a795 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 4 Jan 2024 00:14:16 +0000 Subject: [PATCH 066/116] Update Rust crate proc-macro2 to 1.0.75 --- derive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 41a00d0..4adf2ca 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -9,6 +9,6 @@ edition = "2021" proc_macro = true [dependencies] -proc-macro2 = "1.0.74" +proc-macro2 = "1.0.75" quote = "1.0.35" syn = { version = "2.0.46", features = ["full"] } From c1ee31aa7cdc8795173d9116799c543121c8e08c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 4 Jan 2024 00:14:19 +0000 Subject: [PATCH 067/116] Update Rust crate syn to 2.0.47 --- derive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 41a00d0..1d108c8 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -11,4 +11,4 @@ proc_macro = true [dependencies] proc-macro2 = "1.0.74" quote = "1.0.35" -syn = { version = "2.0.46", features = ["full"] } +syn = { version = "2.0.47", features = ["full"] } From c68ae76e0f7fa152dddf77f7f1f19ef236beb711 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 4 Jan 2024 19:11:27 +0000 Subject: [PATCH 068/116] Update Rust crate syn to 2.0.48 --- derive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 0355185..6ba12d0 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -11,4 +11,4 @@ proc_macro = true [dependencies] proc-macro2 = "1.0.75" quote = "1.0.35" -syn = { version = "2.0.47", features = ["full"] } +syn = { version = "2.0.48", features = ["full"] } From 2fccafda996cf4fab737c1a460e87c481a71a6be Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 6 Jan 2024 05:06:05 +0000 Subject: [PATCH 069/116] Update Rust crate proc-macro2 to 1.0.76 --- derive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 6ba12d0..23527c8 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -9,6 +9,6 @@ edition = "2021" proc_macro = true [dependencies] -proc-macro2 = "1.0.75" +proc-macro2 = "1.0.76" quote = "1.0.35" syn = { version = "2.0.48", features = ["full"] } From 008ade0c5dd702a728de1e22734d26ee9cb43367 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 7 Jan 2024 14:08:51 +0000 Subject: [PATCH 070/116] Update Rust crate strsim to 0.10.1 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 138b034..5ae5411 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ readme = "README.md" [dependencies] uutils-args-derive = { version = "0.1.0", path = "derive" } uutils-args-complete = { version = "0.1.0", path = "complete", optional = true } -strsim = "0.10.0" +strsim = "0.10.1" lexopt = "0.3.0" [features] From 066d69c4761fbbcfbde1703906c71472237ae3d6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 8 Jan 2024 01:40:19 +0000 Subject: [PATCH 071/116] Update Rust crate strsim to 0.11.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 5ae5411..c815db7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ readme = "README.md" [dependencies] uutils-args-derive = { version = "0.1.0", path = "derive" } uutils-args-complete = { version = "0.1.0", path = "complete", optional = true } -strsim = "0.10.1" +strsim = "0.11.0" lexopt = "0.3.0" [features] From 720a80e71aeaf7ff7e76837c53babdbbc42dedac Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Wed, 10 Jan 2024 13:08:23 +0100 Subject: [PATCH 072/116] add justfile --- Justfile | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 Justfile diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..7790af0 --- /dev/null +++ b/Justfile @@ -0,0 +1,5 @@ +check: + cargo fmt --all + cargo test --features complete + cargo clippy --all-targets --features complete --workspace -- -D warnings + cargo doc From ea822bb51cc3d4cda212ee295dc83d36d63bea23 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 21 Jan 2024 03:03:12 +0000 Subject: [PATCH 073/116] Update Rust crate proc-macro2 to 1.0.78 --- derive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 23527c8..a18c061 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -9,6 +9,6 @@ edition = "2021" proc_macro = true [dependencies] -proc-macro2 = "1.0.76" +proc-macro2 = "1.0.78" quote = "1.0.35" syn = { version = "2.0.48", features = ["full"] } From ffe79c71f63037a692959aee8e24af2f801feebc Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Mon, 22 Jan 2024 10:24:12 +0100 Subject: [PATCH 074/116] ci: remove actions-rs/cargo & use cargo directly --- .github/workflows/ci.yml | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3e5f46..29a2eae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,10 +20,7 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - - uses: actions-rs/cargo@v1 - with: - command: test - args: --features complete --workspace + - run: cargo test --features complete --workspace rustfmt: name: Rustfmt @@ -37,10 +34,7 @@ jobs: components: rustfmt - uses: Swatinem/rust-cache@v2 - name: Check formatting - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all -- --check + run: cargo fmt --all -- --check clippy: name: Clippy @@ -54,10 +48,7 @@ jobs: components: clippy - uses: Swatinem/rust-cache@v2 - name: Clippy check - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --all-targets --features complete --workspace -- -D warnings + run: cargo clippy --all-targets --features complete --workspace -- -D warnings docs: name: Docs @@ -71,6 +62,4 @@ jobs: - name: Check documentation env: RUSTDOCFLAGS: -D warnings - uses: actions-rs/cargo@v1 - with: - command: doc + run: cargo doc From c6d692c8b496a597c8ee7d52955d0273cd68155d Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Fri, 22 Dec 2023 13:34:50 +0100 Subject: [PATCH 075/116] new positional argument system --- src/lib.rs | 1 + src/positional.rs | 400 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 401 insertions(+) create mode 100644 src/positional.rs diff --git a/src/lib.rs b/src/lib.rs index 694bcdf..6fdfd42 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,7 @@ mod error; pub mod internal; +pub mod positional; mod value; #[cfg(doc)] diff --git a/src/positional.rs b/src/positional.rs new file mode 100644 index 0000000..b3af7e2 --- /dev/null +++ b/src/positional.rs @@ -0,0 +1,400 @@ +//! Parsing of positional arguments. +//! +//! The signature for parsing positional arguments is one of [`&'static str`], +//! [`Opt`], [`Many`] or a tuple of those. The [`Unpack::unpack`] method of +//! these types parses a `Vec` into the corresponding +//! [`Unpack::Output`] type. +//! +//! For example: +//! ``` +//! use std::ffi::OsString; +//! use uutils_args::positional::{Opt, Unpack}; +//! +//! let (a, b) = ("FILE1", Opt("FILE2")).unpack(vec![OsString::from("one")]).unwrap(); +//! assert_eq!(a, OsString::from("one")); +//! assert_eq!(b, None); +//! +//! let (a, b) = ("FILE1", Opt("FILE2")).unpack(vec![OsString::from("one"), OsString::from("two")]).unwrap(); +//! assert_eq!(a, OsString::from("one")); +//! assert_eq!(b, Some(OsString::from("two"))); +//! ``` +//! +//! Here are a few examples: +//! +//! ```ignore +//! () // no arguments +//! "FOO" // one required argument with output `OsString` +//! Opt("FOO") // one optional argument with output `Option` +//! Many("FOO") // one or more arguments with output `Vec` +//! Opt(Many("FOO")) // zero or more arguments with output `Vec` +//! ("FOO", "FOO") // two required arguments with output (`OsString`, `OsString`) +//! ``` +//! +//! This allows for the construction of complex signatures. The signature +//! +//! ```ignore +//! ("FOO", Opt(Many("BAR"))) +//! ``` +//! +//! specifies that there is first a required argument "FOO" and any number of +//! values for "BAR". +//! +//! However, not all combinations are supported by design. For example, the +//! signature +//! +//! ```ignore +//! (Many("FOO"), Many("BAR")) +//! ``` +//! +//! does not make sense, because it's unclear where the positional arguments +//! should go. The supported tuples implement [`Unpack`]. + +use crate::error::{Error, ErrorKind}; +use std::ffi::OsString; + +/// A required argument +type Req = &'static str; + +/// Makes it's argument optional +pub struct Opt(pub T); + +/// 1 or more arguments +pub struct Many(pub Req); + +/// Unpack a `Vec` into the output type +/// +/// See the [module documentation](crate::positional) for more information. +pub trait Unpack { + type Output: ToOptional; + fn unpack(&self, operands: Vec) -> Result; +} + +impl Unpack for () { + type Output = (); + + fn unpack(&self, operands: Vec) -> Result { + assert_empty(operands) + } +} + +impl Unpack for (T,) { + type Output = T::Output; + + fn unpack(&self, operands: Vec) -> Result { + self.0.unpack(operands) + } +} + +impl Unpack for Req { + type Output = OsString; + + fn unpack(&self, mut operands: Vec) -> Result { + let arg = pop_front(self, &mut operands)?; + assert_empty(operands)?; + Ok(arg) + } +} + +impl Unpack for Opt +where + T::Output: ToOptional, +{ + type Output = ::Out; + + fn unpack(&self, operands: Vec) -> Result { + Ok(if operands.is_empty() { + ::none() + } else { + self.0.unpack(operands)?.some() + }) + } +} + +impl Unpack for Many { + type Output = Vec; + + fn unpack(&self, operands: Vec) -> Result { + if operands.is_empty() { + return Err(Error { + exit_code: 1, + kind: ErrorKind::MissingPositionalArguments(vec![self.0.into()]), + }); + } + Ok(operands) + } +} + +impl Unpack for (Req, T) { + type Output = (OsString, T::Output); + + fn unpack(&self, mut operands: Vec) -> Result { + let arg = pop_front(self.0, &mut operands)?; + let rest = self.1.unpack(operands)?; + Ok((arg, rest)) + } +} + +impl Unpack for (Req, Req, T) { + type Output = (OsString, OsString, T::Output); + + fn unpack(&self, mut operands: Vec) -> Result { + let arg1 = pop_front(self.0, &mut operands)?; + let arg2 = pop_front(self.1, &mut operands)?; + let rest = self.2.unpack(operands)?; + Ok((arg1, arg2, rest)) + } +} + +impl Unpack for (Opt, Req) { + type Output = ( as Unpack>::Output, ::Output); + + fn unpack(&self, mut operands: Vec) -> Result { + let arg = pop_back(self.1, &mut operands)?; + let rest = self.0.unpack(operands)?; + Ok((rest, arg)) + } +} + +impl Unpack for (Many, Req) { + type Output = (::Output, ::Output); + + fn unpack(&self, mut operands: Vec) -> Result { + let arg = pop_back(self.1, &mut operands)?; + let rest = self.0.unpack(operands)?; + Ok((rest, arg)) + } +} + +fn pop_front(name: &str, operands: &mut Vec) -> Result { + if operands.is_empty() { + return Err(Error { + exit_code: 1, + kind: ErrorKind::MissingPositionalArguments(vec![name.into()]), + }); + } + Ok(operands.remove(0)) +} + +fn pop_back(name: &str, operands: &mut Vec) -> Result { + operands.pop().ok_or_else(|| Error { + exit_code: 1, + kind: ErrorKind::MissingPositionalArguments(vec![name.into()]), + }) +} + +fn assert_empty(mut operands: Vec) -> Result<(), Error> { + if let Some(arg) = operands.pop() { + return Err(Error { + exit_code: 1, + kind: ErrorKind::UnexpectedArgument(arg), + }); + } + Ok(()) +} + +pub trait ToOptional { + type Out: ToOptional; + fn some(self) -> Self::Out; + fn none() -> Self::Out; +} + +impl ToOptional for OsString { + type Out = Option; + fn some(self) -> Self::Out { + Some(self) + } + fn none() -> Self::Out { + None + } +} + +impl ToOptional for () { + type Out = Option; + fn some(self) -> Self::Out { + Some(self) + } + fn none() -> Self::Out { + None + } +} + +impl ToOptional for Vec { + type Out = Self; + fn some(self) -> Self::Out { + self + } + fn none() -> Self::Out { + Vec::new() + } +} + +impl ToOptional for (T1, T2) { + type Out = Option; + fn some(self) -> Self::Out { + Some(self) + } + fn none() -> Self::Out { + None + } +} + +impl ToOptional for (T1, T2, T3) { + type Out = Option; + fn some(self) -> Self::Out { + Some(self) + } + fn none() -> Self::Out { + None + } +} + +impl ToOptional for Option { + type Out = Self; + fn some(self) -> Self::Out { + self + } + fn none() -> Self::Out { + None + } +} + +#[cfg(test)] +mod test { + use super::{Many, Opt, Unpack}; + use std::ffi::OsString; + + macro_rules! a { + ($e:expr, $t:ty) => { + let _: Result<$t, _> = $e.unpack(Vec::new()); + }; + } + + #[track_caller] + fn assert_ok(signature: &U, expected: U::Output, operands: [&str; N]) + where + U::Output: Eq + std::fmt::Debug, + { + let operands = operands.into_iter().map(Into::into).collect(); + assert_eq!(signature.unpack(operands).unwrap(), expected); + } + + #[track_caller] + fn assert_err(signature: &impl Unpack, operands: [&str; N]) { + let operands = operands.into_iter().map(Into::into).collect(); + assert!(signature.unpack(operands).is_err()); + } + + #[test] + fn compile_tests() { + // The five basic ones + a!((), ()); + a!("FOO", OsString); + a!(Opt("FOO"), Option); + a!(Many("FOO"), Vec); + a!(Opt(Many("FOO")), Vec); + + // Start building some tuples + a!(("FOO", "BAR"), (OsString, OsString)); + a!(("FOO", Opt("BAR")), (OsString, Option)); + a!(("FOO", Many("BAR")), (OsString, Vec)); + a!(("FOO", Opt(Many("BAR"))), (OsString, Vec)); + + // The other way around! + a!((Opt("FOO"), "BAR"), (Option, OsString)); + a!((Many("FOO"), "BAR"), (Vec, OsString)); + a!((Opt(Many("FOO")), "BAR"), (Vec, OsString)); + + // Longer tuples! + a!(("FOO", "BAR", "BAZ"), (OsString, OsString, OsString)); + a!( + ("FOO", "BAR", Opt("BAZ")), + (OsString, OsString, Option) + ); + a!( + ("FOO", "BAR", Many("BAZ")), + (OsString, OsString, Vec) + ); + a!( + ("FOO", "BAR", Opt(Many("BAZ"))), + (OsString, OsString, Vec) + ); + + // seq [FIRST [INCREMENT]] LAST + a!( + (Opt(("FIRST", Opt("INCREMENT"))), "LAST"), + (Option<(OsString, Option)>, OsString) + ); + + // mknod NAME TYPE [MAJOR MINOR] + a!( + ("NAME", "TYPE", Opt(("MAJOR", "MINOR"))), + (OsString, OsString, Option<(OsString, OsString)>) + ); + + // chroot + a!( + ("NEWROOT", Opt(("COMMAND", Opt(Many("ARG"))))), + (OsString, Option<(OsString, Vec)>) + ); + } + + #[test] + fn unit() { + assert_ok(&(), (), []); + assert_err(&(), ["foo"]); + assert_err(&(), ["foo", "bar"]); + } + + #[test] + fn required() { + let s = "FOO"; + assert_err(&s, []); + assert_ok(&s, "foo".into(), ["foo"]); + assert_err(&s, ["foo", "bar"]); + assert_err(&s, ["foo", "bar", "baz"]); + } + + #[test] + fn optional() { + let s = Opt("FOO"); + assert_ok(&s, None, []); + assert_ok(&s, Some("foo".into()), ["foo"]); + assert_err(&s, ["foo", "bar"]); + assert_err(&s, ["foo", "bar", "baz"]); + } + + #[test] + fn many() { + let s = Many("FOO"); + assert_err(&s, []); + assert_ok(&s, vec!["foo".into()], ["foo"]); + assert_ok(&s, vec!["foo".into(), "bar".into()], ["foo", "bar"]); + assert_ok( + &s, + vec!["foo".into(), "bar".into(), "baz".into()], + ["foo", "bar", "baz"], + ); + } + + #[test] + fn opt_many() { + let s = Opt(Many("FOO")); + assert_ok(&s, vec![], []); + assert_ok(&s, vec!["foo".into()], ["foo"]); + assert_ok(&s, vec!["foo".into(), "bar".into()], ["foo", "bar"]); + assert_ok( + &s, + vec!["foo".into(), "bar".into(), "baz".into()], + ["foo", "bar", "baz"], + ); + } + + #[test] + fn req_req() { + let s = ("FOO", "BAR"); + assert_err(&s, []); + assert_err(&s, ["foo"]); + assert_ok(&s, ("foo".into(), "bar".into()), ["foo", "bar"]); + assert_err(&s, ["foo", "bar", "baz"]); + } +} From 291beb3b3b3c5d91fdcaf5122e16ed351c99a9bb Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Fri, 22 Dec 2023 14:16:58 +0100 Subject: [PATCH 076/116] make the positional system generic over Vec --- src/error.rs | 8 +- src/positional.rs | 287 ++++++++++++++++++---------------------------- 2 files changed, 119 insertions(+), 176 deletions(-) diff --git a/src/error.rs b/src/error.rs index ff7a6f4..0da36e5 100644 --- a/src/error.rs +++ b/src/error.rs @@ -28,7 +28,7 @@ pub enum ErrorKind { UnexpectedOption(String, Vec), /// No more positional arguments were expected, but one was given anyway. - UnexpectedArgument(OsString), + UnexpectedArgument(String), /// A value was passed to an option that didn't expect a value. UnexpectedValue { @@ -99,7 +99,7 @@ impl Display for ErrorKind { Ok(()) } ErrorKind::UnexpectedArgument(arg) => { - write!(f, "Found an invalid argument '{}'.", arg.to_string_lossy()) + write!(f, "Found an invalid argument '{}'.", arg) } ErrorKind::UnexpectedValue { option, value } => { write!( @@ -144,7 +144,9 @@ impl From for ErrorKind { match other { lexopt::Error::MissingValue { option } => Self::MissingValue { option }, lexopt::Error::UnexpectedOption(s) => Self::UnexpectedOption(s, Vec::new()), - lexopt::Error::UnexpectedArgument(s) => Self::UnexpectedArgument(s), + lexopt::Error::UnexpectedArgument(s) => { + Self::UnexpectedArgument(s.to_string_lossy().to_string()) + } lexopt::Error::UnexpectedValue { option, value } => { Self::UnexpectedValue { option, value } } diff --git a/src/positional.rs b/src/positional.rs index b3af7e2..c2dbaee 100644 --- a/src/positional.rs +++ b/src/positional.rs @@ -1,22 +1,27 @@ //! Parsing of positional arguments. //! -//! The signature for parsing positional arguments is one of [`&'static str`], -//! [`Opt`], [`Many`] or a tuple of those. The [`Unpack::unpack`] method of -//! these types parses a `Vec` into the corresponding -//! [`Unpack::Output`] type. +//! The signature for parsing positional arguments is one of `&'static str`, +//! [`Opt`], [`Many0`], [`Many1`] or a tuple of those. The [`Unpack::unpack`] +//! method of these types parses a `Vec` into the corresponding +//! [`Unpack::Output`] type. //! //! For example: //! ``` //! use std::ffi::OsString; //! use uutils_args::positional::{Opt, Unpack}; //! -//! let (a, b) = ("FILE1", Opt("FILE2")).unpack(vec![OsString::from("one")]).unwrap(); -//! assert_eq!(a, OsString::from("one")); +//! let (a, b) = ("FILE1", Opt("FILE2")).unpack(vec!["one"]).unwrap(); +//! assert_eq!(a, "one"); //! assert_eq!(b, None); //! -//! let (a, b) = ("FILE1", Opt("FILE2")).unpack(vec![OsString::from("one"), OsString::from("two")]).unwrap(); -//! assert_eq!(a, OsString::from("one")); -//! assert_eq!(b, Some(OsString::from("two"))); +//! let (a, b) = ("FILE1", Opt("FILE2")).unpack(vec!["one", "two"]).unwrap(); +//! assert_eq!(a, "one"); +//! assert_eq!(b, Some("two")); +//! +//! // It works for any `Vec`: +//! let (a, b) = ("FILE1", Opt("FILE2")).unpack(vec![1, 2]).unwrap(); +//! assert_eq!(a, 1); +//! assert_eq!(b, Some(2)); //! ``` //! //! Here are a few examples: @@ -50,7 +55,7 @@ //! should go. The supported tuples implement [`Unpack`]. use crate::error::{Error, ErrorKind}; -use std::ffi::OsString; +use std::fmt::Debug; /// A required argument type Req = &'static str; @@ -59,61 +64,69 @@ type Req = &'static str; pub struct Opt(pub T); /// 1 or more arguments -pub struct Many(pub Req); +pub struct Many1(pub Req); + +/// 0 or more arguments +pub struct Many0(pub Req); /// Unpack a `Vec` into the output type /// /// See the [module documentation](crate::positional) for more information. pub trait Unpack { - type Output: ToOptional; - fn unpack(&self, operands: Vec) -> Result; + type Output; + fn unpack(&self, operands: Vec) -> Result, Error>; } impl Unpack for () { - type Output = (); + type Output = (); - fn unpack(&self, operands: Vec) -> Result { + fn unpack(&self, operands: Vec) -> Result, Error> { assert_empty(operands) } } -impl Unpack for (T,) { - type Output = T::Output; +impl Unpack for (U,) { + type Output = U::Output; - fn unpack(&self, operands: Vec) -> Result { + fn unpack(&self, operands: Vec) -> Result, Error> { self.0.unpack(operands) } } impl Unpack for Req { - type Output = OsString; + type Output = T; - fn unpack(&self, mut operands: Vec) -> Result { + fn unpack(&self, mut operands: Vec) -> Result, Error> { let arg = pop_front(self, &mut operands)?; assert_empty(operands)?; Ok(arg) } } -impl Unpack for Opt -where - T::Output: ToOptional, -{ - type Output = ::Out; +impl Unpack for Opt { + type Output = Option>; - fn unpack(&self, operands: Vec) -> Result { + fn unpack(&self, operands: Vec) -> Result, Error> { Ok(if operands.is_empty() { - ::none() + None } else { - self.0.unpack(operands)?.some() + Some(self.0.unpack(operands)?) }) } } -impl Unpack for Many { - type Output = Vec; +impl Unpack for Many0 { + type Output = Vec; + + fn unpack(&self, operands: Vec) -> Result, Error> { + Ok(operands) + } +} + +impl Unpack for Many1 { + type Output = Vec; - fn unpack(&self, operands: Vec) -> Result { + fn unpack(&self, operands: Vec) -> Result, Error> { if operands.is_empty() { return Err(Error { exit_code: 1, @@ -124,20 +137,20 @@ impl Unpack for Many { } } -impl Unpack for (Req, T) { - type Output = (OsString, T::Output); +impl Unpack for (Req, U) { + type Output = (T, U::Output); - fn unpack(&self, mut operands: Vec) -> Result { + fn unpack(&self, mut operands: Vec) -> Result, Error> { let arg = pop_front(self.0, &mut operands)?; let rest = self.1.unpack(operands)?; Ok((arg, rest)) } } -impl Unpack for (Req, Req, T) { - type Output = (OsString, OsString, T::Output); +impl Unpack for (Req, Req, U) { + type Output = (T, T, U::Output); - fn unpack(&self, mut operands: Vec) -> Result { + fn unpack(&self, mut operands: Vec) -> Result, Error> { let arg1 = pop_front(self.0, &mut operands)?; let arg2 = pop_front(self.1, &mut operands)?; let rest = self.2.unpack(operands)?; @@ -145,142 +158,87 @@ impl Unpack for (Req, Req, T) { } } -impl Unpack for (Opt, Req) { - type Output = ( as Unpack>::Output, ::Output); +impl Unpack for (Opt, Req) { + type Output = (Option<::Output>, T); + + fn unpack(&self, mut operands: Vec) -> Result, Error> { + let arg = pop_back(self.1, &mut operands)?; + let rest = self.0.unpack(operands)?; + Ok((rest, arg)) + } +} + +impl Unpack for (Many0, Req) { + type Output = (Vec, T); - fn unpack(&self, mut operands: Vec) -> Result { + fn unpack(&self, mut operands: Vec) -> Result, Error> { let arg = pop_back(self.1, &mut operands)?; let rest = self.0.unpack(operands)?; Ok((rest, arg)) } } -impl Unpack for (Many, Req) { - type Output = (::Output, ::Output); +impl Unpack for (Many1, Req) { + type Output = (Vec, T); - fn unpack(&self, mut operands: Vec) -> Result { + fn unpack(&self, mut operands: Vec) -> Result, Error> { let arg = pop_back(self.1, &mut operands)?; let rest = self.0.unpack(operands)?; Ok((rest, arg)) } } -fn pop_front(name: &str, operands: &mut Vec) -> Result { +fn pop_front(name: &str, operands: &mut Vec) -> Result { if operands.is_empty() { return Err(Error { exit_code: 1, - kind: ErrorKind::MissingPositionalArguments(vec![name.into()]), + kind: ErrorKind::MissingPositionalArguments(vec![name.to_string()]), }); } Ok(operands.remove(0)) } -fn pop_back(name: &str, operands: &mut Vec) -> Result { +fn pop_back(name: &str, operands: &mut Vec) -> Result { operands.pop().ok_or_else(|| Error { exit_code: 1, - kind: ErrorKind::MissingPositionalArguments(vec![name.into()]), + kind: ErrorKind::MissingPositionalArguments(vec![name.to_string()]), }) } -fn assert_empty(mut operands: Vec) -> Result<(), Error> { +fn assert_empty(mut operands: Vec) -> Result<(), Error> { if let Some(arg) = operands.pop() { return Err(Error { exit_code: 1, - kind: ErrorKind::UnexpectedArgument(arg), + kind: ErrorKind::UnexpectedArgument(format!("{:?}", arg)), }); } Ok(()) } -pub trait ToOptional { - type Out: ToOptional; - fn some(self) -> Self::Out; - fn none() -> Self::Out; -} - -impl ToOptional for OsString { - type Out = Option; - fn some(self) -> Self::Out { - Some(self) - } - fn none() -> Self::Out { - None - } -} - -impl ToOptional for () { - type Out = Option; - fn some(self) -> Self::Out { - Some(self) - } - fn none() -> Self::Out { - None - } -} - -impl ToOptional for Vec { - type Out = Self; - fn some(self) -> Self::Out { - self - } - fn none() -> Self::Out { - Vec::new() - } -} - -impl ToOptional for (T1, T2) { - type Out = Option; - fn some(self) -> Self::Out { - Some(self) - } - fn none() -> Self::Out { - None - } -} - -impl ToOptional for (T1, T2, T3) { - type Out = Option; - fn some(self) -> Self::Out { - Some(self) - } - fn none() -> Self::Out { - None - } -} - -impl ToOptional for Option { - type Out = Self; - fn some(self) -> Self::Out { - self - } - fn none() -> Self::Out { - None - } -} - #[cfg(test)] mod test { - use super::{Many, Opt, Unpack}; - use std::ffi::OsString; + use super::{Many0, Many1, Opt, Unpack}; macro_rules! a { ($e:expr, $t:ty) => { - let _: Result<$t, _> = $e.unpack(Vec::new()); + let _: Result<$t, _> = $e.unpack(Vec::<&str>::new()); }; } #[track_caller] - fn assert_ok(signature: &U, expected: U::Output, operands: [&str; N]) - where - U::Output: Eq + std::fmt::Debug, + fn assert_ok<'a, U: Unpack, const N: usize>( + signature: &U, + expected: U::Output<&'a str>, + operands: [&'a str; N], + ) where + U::Output<&'a str>: Eq + std::fmt::Debug, { - let operands = operands.into_iter().map(Into::into).collect(); - assert_eq!(signature.unpack(operands).unwrap(), expected); + assert_eq!(signature.unpack(Vec::from(operands)).unwrap(), expected); } #[track_caller] fn assert_err(signature: &impl Unpack, operands: [&str; N]) { - let operands = operands.into_iter().map(Into::into).collect(); + let operands = Vec::from(operands); assert!(signature.unpack(operands).is_err()); } @@ -288,53 +246,44 @@ mod test { fn compile_tests() { // The five basic ones a!((), ()); - a!("FOO", OsString); - a!(Opt("FOO"), Option); - a!(Many("FOO"), Vec); - a!(Opt(Many("FOO")), Vec); + a!("FOO", &str); + a!(Opt("FOO"), Option<&str>); + a!(Many0("FOO"), Vec<&str>); + a!(Many1("FOO"), Vec<&str>); // Start building some tuples - a!(("FOO", "BAR"), (OsString, OsString)); - a!(("FOO", Opt("BAR")), (OsString, Option)); - a!(("FOO", Many("BAR")), (OsString, Vec)); - a!(("FOO", Opt(Many("BAR"))), (OsString, Vec)); + a!(("FOO", "BAR"), (&str, &str)); + a!(("FOO", Opt("BAR")), (&str, Option<&str>)); + a!(("FOO", Many0("BAR")), (&str, Vec<&str>)); + a!(("FOO", Many1("BAR")), (&str, Vec<&str>)); // The other way around! - a!((Opt("FOO"), "BAR"), (Option, OsString)); - a!((Many("FOO"), "BAR"), (Vec, OsString)); - a!((Opt(Many("FOO")), "BAR"), (Vec, OsString)); + a!((Opt("FOO"), "BAR"), (Option<&str>, &str)); + a!((Many0("FOO"), "BAR"), (Vec<&str>, &str)); + a!((Many1("FOO"), "BAR"), (Vec<&str>, &str)); // Longer tuples! - a!(("FOO", "BAR", "BAZ"), (OsString, OsString, OsString)); - a!( - ("FOO", "BAR", Opt("BAZ")), - (OsString, OsString, Option) - ); - a!( - ("FOO", "BAR", Many("BAZ")), - (OsString, OsString, Vec) - ); - a!( - ("FOO", "BAR", Opt(Many("BAZ"))), - (OsString, OsString, Vec) - ); + a!(("FOO", "BAR", "BAZ"), (&str, &str, &str)); + a!(("FOO", "BAR", Opt("BAZ")), (&str, &str, Option<&str>)); + a!(("FOO", "BAR", Many0("BAZ")), (&str, &str, Vec<&str>)); + a!(("FOO", "BAR", Many1("BAZ")), (&str, &str, Vec<&str>)); // seq [FIRST [INCREMENT]] LAST a!( (Opt(("FIRST", Opt("INCREMENT"))), "LAST"), - (Option<(OsString, Option)>, OsString) + (Option<(&str, Option<&str>)>, &str) ); // mknod NAME TYPE [MAJOR MINOR] a!( ("NAME", "TYPE", Opt(("MAJOR", "MINOR"))), - (OsString, OsString, Option<(OsString, OsString)>) + (&str, &str, Option<(&str, &str)>) ); // chroot a!( - ("NEWROOT", Opt(("COMMAND", Opt(Many("ARG"))))), - (OsString, Option<(OsString, Vec)>) + ("NEWROOT", Opt(("COMMAND", Many0("ARG")))), + (&str, Option<(&str, Vec<&str>)>) ); } @@ -349,7 +298,7 @@ mod test { fn required() { let s = "FOO"; assert_err(&s, []); - assert_ok(&s, "foo".into(), ["foo"]); + assert_ok(&s, "foo", ["foo"]); assert_err(&s, ["foo", "bar"]); assert_err(&s, ["foo", "bar", "baz"]); } @@ -358,35 +307,27 @@ mod test { fn optional() { let s = Opt("FOO"); assert_ok(&s, None, []); - assert_ok(&s, Some("foo".into()), ["foo"]); + assert_ok(&s, Some("foo"), ["foo"]); assert_err(&s, ["foo", "bar"]); assert_err(&s, ["foo", "bar", "baz"]); } #[test] - fn many() { - let s = Many("FOO"); + fn many1() { + let s = Many1("FOO"); assert_err(&s, []); - assert_ok(&s, vec!["foo".into()], ["foo"]); - assert_ok(&s, vec!["foo".into(), "bar".into()], ["foo", "bar"]); - assert_ok( - &s, - vec!["foo".into(), "bar".into(), "baz".into()], - ["foo", "bar", "baz"], - ); + assert_ok(&s, vec!["foo"], ["foo"]); + assert_ok(&s, vec!["foo", "bar"], ["foo", "bar"]); + assert_ok(&s, vec!["foo", "bar", "baz"], ["foo", "bar", "baz"]); } #[test] - fn opt_many() { - let s = Opt(Many("FOO")); + fn many0() { + let s = Many0("FOO"); assert_ok(&s, vec![], []); - assert_ok(&s, vec!["foo".into()], ["foo"]); - assert_ok(&s, vec!["foo".into(), "bar".into()], ["foo", "bar"]); - assert_ok( - &s, - vec!["foo".into(), "bar".into(), "baz".into()], - ["foo", "bar", "baz"], - ); + assert_ok(&s, vec!["foo"], ["foo"]); + assert_ok(&s, vec!["foo", "bar"], ["foo", "bar"]); + assert_ok(&s, vec!["foo", "bar", "baz"], ["foo", "bar", "baz"]); } #[test] @@ -394,7 +335,7 @@ mod test { let s = ("FOO", "BAR"); assert_err(&s, []); assert_err(&s, ["foo"]); - assert_ok(&s, ("foo".into(), "bar".into()), ["foo", "bar"]); + assert_ok(&s, ("foo", "bar"), ["foo", "bar"]); assert_err(&s, ["foo", "bar", "baz"]); } } From a31f9bef3978604fb43bc1f612023936eef42ef2 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Fri, 22 Dec 2023 15:00:10 +0100 Subject: [PATCH 077/116] test the new positional argument system in coreutils tests --- tests/coreutils/base32.rs | 50 ++++++++++++++++++++----------------- tests/coreutils/basename.rs | 32 +++++++++++++++--------- tests/coreutils/ls.rs | 2 +- tests/coreutils/mktemp.rs | 47 +++++++++++++++++++++++----------- 4 files changed, 81 insertions(+), 50 deletions(-) diff --git a/tests/coreutils/base32.rs b/tests/coreutils/base32.rs index de32e74..24ce251 100644 --- a/tests/coreutils/base32.rs +++ b/tests/coreutils/base32.rs @@ -1,4 +1,9 @@ -use uutils_args::{Arguments, Options}; +use std::ffi::OsString; + +use uutils_args::{ + positional::{Opt, Unpack}, + Arguments, Options, +}; #[derive(Clone, Arguments)] enum Arg { @@ -39,30 +44,29 @@ impl Options for Settings { } } +fn parse(args: I) -> Result<(Settings, Option), uutils_args::Error> +where + I: IntoIterator, + I::Item: Into, +{ + let (s, ops) = Settings::default().parse(args)?; + let file = Opt("FILE").unpack(ops)?; + Ok((s, file)) +} + #[test] fn wrap() { + assert_eq!(parse(["base32"]).unwrap().0.wrap, Some(76)); + assert_eq!(parse(["base32", "-w0"]).unwrap().0.wrap, None); + assert_eq!(parse(["base32", "-w100"]).unwrap().0.wrap, Some(100)); + assert_eq!(parse(["base32", "--wrap=100"]).unwrap().0.wrap, Some(100)); +} + +#[test] +fn file() { + assert_eq!(parse(["base32"]).unwrap().1, None); assert_eq!( - Settings::default().parse(["base32"]).unwrap().0.wrap, - Some(76) - ); - assert_eq!( - Settings::default().parse(["base32", "-w0"]).unwrap().0.wrap, - None - ); - assert_eq!( - Settings::default() - .parse(["base32", "-w100"]) - .unwrap() - .0 - .wrap, - Some(100) - ); - assert_eq!( - Settings::default() - .parse(["base32", "--wrap=100"]) - .unwrap() - .0 - .wrap, - Some(100) + parse(["base32", "file"]).unwrap().1, + Some(OsString::from("file")) ); } diff --git a/tests/coreutils/basename.rs b/tests/coreutils/basename.rs index 7f0716e..9324fe3 100644 --- a/tests/coreutils/basename.rs +++ b/tests/coreutils/basename.rs @@ -1,6 +1,9 @@ use std::ffi::OsString; -use uutils_args::{Arguments, Options}; +use uutils_args::{ + positional::{Many1, Unpack}, + Arguments, Options, +}; #[derive(Clone, Arguments)] enum Arg { @@ -35,19 +38,24 @@ impl Options for Settings { } } -fn parse(args: &[&str]) -> Settings { - let (mut settings, operands) = Settings::default().parse(args).unwrap(); - settings.names = operands; - if !settings.multiple { - assert_eq!(settings.names.len(), 2); - settings.suffix = settings.names.pop().unwrap(); +fn parse(args: &[&str]) -> Result { + let (mut settings, operands) = Settings::default().parse(args)?; + + if settings.multiple { + let names = Many1("FILE").unpack(operands)?; + settings.names = names; + } else { + let (names, suffix) = ("FILE", "SUFFIX").unpack(operands)?; + settings.names = vec![names]; + settings.suffix = suffix; } - settings + + Ok(settings) } #[test] fn name_and_suffix() { - let settings = parse(&["basename", "foobar", "bar"]); + let settings = parse(&["basename", "foobar", "bar"]).unwrap(); assert!(!settings.zero); assert_eq!(settings.names, vec!["foobar"]); assert_eq!(settings.suffix, "bar"); @@ -55,7 +63,7 @@ fn name_and_suffix() { #[test] fn zero_name_and_suffix() { - let settings = parse(&["basename", "-z", "foobar", "bar"]); + let settings = parse(&["basename", "-z", "foobar", "bar"]).unwrap(); assert!(settings.zero); assert_eq!(settings.names, vec!["foobar"]); assert_eq!(settings.suffix, "bar"); @@ -63,7 +71,7 @@ fn zero_name_and_suffix() { #[test] fn all_and_names() { - let settings = parse(&["basename", "-a", "foobar", "bar"]); + let settings = parse(&["basename", "-a", "foobar", "bar"]).unwrap(); assert!(settings.multiple); assert!(!settings.zero); assert_eq!(settings.names, vec!["foobar", "bar"]); @@ -72,7 +80,7 @@ fn all_and_names() { #[test] fn option_like_names() { - let settings = parse(&["basename", "-a", "--", "-a", "-z", "--suffix=SUFFIX"]); + let settings = parse(&["basename", "-a", "--", "-a", "-z", "--suffix=SUFFIX"]).unwrap(); assert!(settings.multiple); assert!(!settings.zero); assert_eq!(settings.names, vec!["-a", "-z", "--suffix=SUFFIX"]); diff --git a/tests/coreutils/ls.rs b/tests/coreutils/ls.rs index 681e83c..9673926 100644 --- a/tests/coreutils/ls.rs +++ b/tests/coreutils/ls.rs @@ -38,7 +38,7 @@ impl When { Self::Always => true, Self::Never => false, // Should be atty::is(atty::Stream::Stdout), but I don't want to - // pull that depenency in just for this test. + // pull that dependency in just for this test. Self::Auto => true, } } diff --git a/tests/coreutils/mktemp.rs b/tests/coreutils/mktemp.rs index 1319709..2410f7d 100644 --- a/tests/coreutils/mktemp.rs +++ b/tests/coreutils/mktemp.rs @@ -1,6 +1,12 @@ -use std::path::{Path, PathBuf}; +use std::{ + ffi::OsString, + path::{Path, PathBuf}, +}; -use uutils_args::{Arguments, Options}; +use uutils_args::{ + positional::{Opt, Unpack}, + Arguments, Options, +}; #[derive(Clone, Arguments)] enum Arg { @@ -46,39 +52,52 @@ impl Options for Settings { } } +fn parse(args: I) -> Result<(Settings, Option), uutils_args::Error> +where + I: IntoIterator, + I::Item: Into, +{ + let (s, ops) = Settings::default().parse(args)?; + let file = Opt("FILE").unpack(ops)?; + Ok((s, file)) +} + #[test] fn suffix() { - let (s, _operands) = Settings::default() - .parse(["mktemp", "--suffix=hello"]) - .unwrap(); + let (s, _template) = parse(["mktemp", "--suffix=hello"]).unwrap(); assert_eq!(s.suffix.unwrap(), "hello"); - let (s, _operands) = Settings::default().parse(["mktemp", "--suffix="]).unwrap(); + let (s, _template) = parse(["mktemp", "--suffix="]).unwrap(); assert_eq!(s.suffix.unwrap(), ""); - let (s, _operands) = Settings::default().parse(["mktemp", "--suffix="]).unwrap(); + let (s, _template) = parse(["mktemp", "--suffix="]).unwrap(); assert_eq!(s.suffix.unwrap(), ""); - let (s, _operands) = Settings::default().parse(["mktemp"]).unwrap(); + let (s, _template) = parse(["mktemp"]).unwrap(); assert_eq!(s.suffix, None); } #[test] fn tmpdir() { - let (s, _operands) = Settings::default().parse(["mktemp", "--tmpdir"]).unwrap(); + let (s, _template) = parse(["mktemp", "--tmpdir"]).unwrap(); assert_eq!(s.tmp_dir.unwrap(), Path::new(".")); - let (s, _operands) = Settings::default().parse(["mktemp", "--tmpdir="]).unwrap(); + let (s, _template) = parse(["mktemp", "--tmpdir="]).unwrap(); assert_eq!(s.tmp_dir.unwrap(), Path::new("")); - let (s, _operands) = Settings::default().parse(["mktemp", "-p", "foo"]).unwrap(); + let (s, _template) = parse(["mktemp", "-p", "foo"]).unwrap(); assert_eq!(s.tmp_dir.unwrap(), Path::new("foo")); - let (s, _operands) = Settings::default().parse(["mktemp", "-pfoo"]).unwrap(); + let (s, _template) = parse(["mktemp", "-pfoo"]).unwrap(); assert_eq!(s.tmp_dir.unwrap(), Path::new("foo")); - let (s, _operands) = Settings::default().parse(["mktemp", "-p", ""]).unwrap(); + let (s, _template) = parse(["mktemp", "-p", ""]).unwrap(); assert_eq!(s.tmp_dir.unwrap(), Path::new("")); - assert!(Settings::default().parse(["mktemp", "-p"]).is_err()); + assert!(parse(["mktemp", "-p"]).is_err()); +} + +#[test] +fn too_many_arguments() { + assert!(parse(["mktemp", "foo", "bar"]).is_err()); } From 50bd56675b0d952b98e4aa2672a493a99d60b4a4 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Fri, 22 Dec 2023 15:24:24 +0100 Subject: [PATCH 078/116] add test for seq and mknod positional args --- src/positional.rs | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/positional.rs b/src/positional.rs index c2dbaee..b8b0640 100644 --- a/src/positional.rs +++ b/src/positional.rs @@ -30,15 +30,15 @@ //! () // no arguments //! "FOO" // one required argument with output `OsString` //! Opt("FOO") // one optional argument with output `Option` -//! Many("FOO") // one or more arguments with output `Vec` -//! Opt(Many("FOO")) // zero or more arguments with output `Vec` +//! Many1("FOO") // one or more arguments with output `Vec` +//! Many0("FOO") // zero or more arguments with output `Vec` //! ("FOO", "FOO") // two required arguments with output (`OsString`, `OsString`) //! ``` //! //! This allows for the construction of complex signatures. The signature //! //! ```ignore -//! ("FOO", Opt(Many("BAR"))) +//! ("FOO", Many0("BAR")) //! ``` //! //! specifies that there is first a required argument "FOO" and any number of @@ -48,7 +48,7 @@ //! signature //! //! ```ignore -//! (Many("FOO"), Many("BAR")) +//! (Many0("FOO"), Many0("BAR")) //! ``` //! //! does not make sense, because it's unclear where the positional arguments @@ -338,4 +338,24 @@ mod test { assert_ok(&s, ("foo", "bar"), ["foo", "bar"]); assert_err(&s, ["foo", "bar", "baz"]); } + + #[test] + fn seq() { + let s = (Opt(("FIRST", Opt("INCREMENT"))), "LAST"); + assert_err(&s, []); + assert_ok(&s, (None, "1"), ["1"]); + assert_ok(&s, (Some(("1", None)), "2"), ["1", "2"]); + assert_ok(&s, (Some(("1", Some("2"))), "3"), ["1", "2", "3"]); + assert_err(&s, ["1", "2", "3", "4"]); + } + + #[test] + fn mknod() { + let s = ("NAME", "TYPE", Opt(("MAJOR", "MINOR"))); + assert_err(&s, []); + assert_err(&s, ["1"]); + assert_ok(&s, ("1", "2", None), ["1", "2"]); + assert_err(&s, ["1", "2", "3"]); + assert_ok(&s, ("1", "2", Some(("3", "4"))), ["1", "2", "3", "4"]); + } } From 5c7bf4e3a7e8e58ad06db2ecce99e249ec93121e Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Wed, 10 Jan 2024 13:07:41 +0100 Subject: [PATCH 079/116] add options_first argument for the arguments attribute --- derive/src/attributes.rs | 5 ++++ derive/src/lib.rs | 16 ++++++++++- src/lib.rs | 4 +++ tests/options_first.rs | 62 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 tests/options_first.rs diff --git a/derive/src/attributes.rs b/derive/src/attributes.rs index fa068b4..a517f14 100644 --- a/derive/src/attributes.rs +++ b/derive/src/attributes.rs @@ -13,6 +13,7 @@ pub struct ArgumentsAttr { pub file: Option, pub exit_code: i32, pub parse_echo_style: bool, + pub options_first: bool, } impl Default for ArgumentsAttr { @@ -23,6 +24,7 @@ impl Default for ArgumentsAttr { file: None, exit_code: 1, parse_echo_style: false, + options_first: false, } } } @@ -55,6 +57,9 @@ impl ArgumentsAttr { "parse_echo_style" => { args.parse_echo_style = true; } + "options_first" => { + args.options_first = true; + } _ => return Err(meta.error("unrecognized argument for arguments attribute")), }; Ok(()) diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 3b2fbdd..5e8d7f4 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -67,6 +67,20 @@ pub fn arguments(input: TokenStream) -> TokenStream { quote!(parser.next()?) }; + // If options_first is set and we find the first positional argument, we + // immediately return all of them. + let positional = if arguments_attr.options_first { + quote!( + // Unwrap is fine because this is called when we have just parsed a + // value and therefore are not partially within an option. + let mut values = parser.raw_args().unwrap().collect::>(); + values.insert(0, value); + Ok(Some(::uutils_args::Argument::MultiPositional(values))) + ) + } else { + quote!(Ok(Some(::uutils_args::Argument::Positional(value)))) + }; + let expanded = quote!( impl #impl_generics Arguments for #name #ty_generics #where_clause { const EXIT_CODE: i32 = #exit_code; @@ -91,7 +105,7 @@ pub fn arguments(input: TokenStream) -> TokenStream { match arg { lexopt::Arg::Short(short) => { #short }, lexopt::Arg::Long(long) => { #long }, - lexopt::Arg::Value(value) => { Ok(Some(::uutils_args::Argument::Positional(value))) }, + lexopt::Arg::Value(value) => { #positional }, } } diff --git a/src/lib.rs b/src/lib.rs index 6fdfd42..5164734 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -65,6 +65,7 @@ pub enum Argument { Help, Version, Positional(OsString), + MultiPositional(Vec), Custom(T), } @@ -143,6 +144,9 @@ impl ArgumentIter { Argument::Positional(arg) => { self.positional_arguments.push(arg); } + Argument::MultiPositional(args) => { + self.positional_arguments.extend(args); + } Argument::Custom(arg) => return Ok(Some(arg)), } } diff --git a/tests/options_first.rs b/tests/options_first.rs new file mode 100644 index 0000000..ee9bc73 --- /dev/null +++ b/tests/options_first.rs @@ -0,0 +1,62 @@ +use std::ffi::OsString; + +use uutils_args::{Arguments, Options}; + +#[test] +fn timeout_like() { + // The timeout coreutil has -v and a command argument + #[derive(Arguments)] + #[arguments(options_first)] + enum Arg { + #[arg("-v", "--verbose")] + Verbose, + } + + #[derive(Default)] + struct Settings { + verbose: bool, + } + + impl Options for Settings { + fn apply(&mut self, arg: Arg) { + match arg { + Arg::Verbose => self.verbose = true, + } + } + } + + let (settings, command) = Settings::default() + .parse(["timeout", "-v", "10", "foo", "-v"]) + .unwrap(); + + assert!(settings.verbose); + assert_eq!( + command, + vec![ + OsString::from("10"), + OsString::from("foo"), + OsString::from("-v") + ] + ); + + let (settings, command) = Settings::default() + .parse(["timeout", "10", "foo", "-v"]) + .unwrap(); + + assert!(!settings.verbose); + assert_eq!( + command, + vec![ + OsString::from("10"), + OsString::from("foo"), + OsString::from("-v") + ] + ); + + let (settings, command) = Settings::default() + .parse(["timeout", "--", "10", "-v"]) + .unwrap(); + + assert!(!settings.verbose); + assert_eq!(command, vec![OsString::from("10"), OsString::from("-v")]); +} From e6814683e8c5b82880e978d48051605b824bc0c6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 16 Feb 2024 01:39:14 +0000 Subject: [PATCH 080/116] fix(deps): update rust crate syn to 2.0.49 --- derive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index a18c061..3fa077c 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -11,4 +11,4 @@ proc_macro = true [dependencies] proc-macro2 = "1.0.78" quote = "1.0.35" -syn = { version = "2.0.48", features = ["full"] } +syn = { version = "2.0.49", features = ["full"] } From 0824862dae2d54bc23d20134e56d3d246553845e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 20 Feb 2024 00:13:56 +0000 Subject: [PATCH 081/116] fix(deps): update rust crate syn to 2.0.50 --- derive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 3fa077c..878d589 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -11,4 +11,4 @@ proc_macro = true [dependencies] proc-macro2 = "1.0.78" quote = "1.0.35" -syn = { version = "2.0.49", features = ["full"] } +syn = { version = "2.0.50", features = ["full"] } From 06329162a548cc88e4a4d5ad55a85d0aa3ff759a Mon Sep 17 00:00:00 2001 From: Ben Wiederhake Date: Tue, 20 Feb 2024 01:48:09 +0100 Subject: [PATCH 082/116] implement proof of concept for shuf echo args / file --- tests/coreutils.rs | 3 + tests/coreutils/shuf.rs | 395 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 398 insertions(+) create mode 100644 tests/coreutils/shuf.rs diff --git a/tests/coreutils.rs b/tests/coreutils.rs index cce04e9..6ebfeac 100644 --- a/tests/coreutils.rs +++ b/tests/coreutils.rs @@ -30,3 +30,6 @@ mod ls; #[path = "coreutils/tail.rs"] mod tail; + +#[path = "coreutils/shuf.rs"] +mod shuf; diff --git a/tests/coreutils/shuf.rs b/tests/coreutils/shuf.rs new file mode 100644 index 0000000..185e0b2 --- /dev/null +++ b/tests/coreutils/shuf.rs @@ -0,0 +1,395 @@ +use std::{ffi::OsString, path::PathBuf}; +use uutils_args::{ + positional::{Many0, Opt, Unpack}, + Arguments, Options, +}; + +#[derive(Clone, Arguments)] +enum Arg { + #[arg("-e", "--echo")] + Echo, + + #[arg("-z", "--zero")] + Zero, + // Not relevant for this example: -i, -n, -r, -o, --random-source +} + +#[derive(Debug, Default, PartialEq)] +struct Settings { + echo: bool, + zero: bool, + echo_args: Vec, + file: Option, +} + +impl Options for Settings { + fn apply(&mut self, arg: Arg) { + match arg { + Arg::Echo => self.echo = true, + Arg::Zero => self.zero = true, + } + } +} + +fn parse(args: &[&str]) -> Result { + let (mut settings, operands) = Settings::default().parse(args)?; + + if settings.echo { + settings.echo_args = Many0("ARG").unpack(operands)?; + } else { + settings.file = Opt("FILE").unpack(operands)?.map(From::::from); + } + + Ok(settings) +} + +#[test] +fn noarg_is_file() { + let settings = parse(&["shuf"]).unwrap(); + assert_eq!(settings, Settings::default()); +} + +#[test] +fn file_takes_one_arg() { + let settings = parse(&["shuf", "myfile"]).unwrap(); + assert_eq!( + settings, + Settings { + file: Some("myfile".into()), + ..Settings::default() + } + ); +} + +#[test] +fn file_refuses_two_files() { + // FIXME: Check detected error + assert!(parse(&["shuf", "myfile", "otherfile"]).is_err()); +} + +#[test] +fn file_refuses_three_files() { + // FIXME: Check detected error + assert!(parse(&["shuf", "myfile", "otherfile", "morefile"]).is_err()); +} + +#[test] +fn noarg_is_file_zero() { + let settings = parse(&["shuf", "-z"]).unwrap(); + assert_eq!( + settings, + Settings { + zero: true, + ..Settings::default() + } + ); +} + +#[test] +fn the_help() { + let settings = parse(&["shuf", "--help"]).unwrap(); + assert_eq!( + settings, + Settings { + zero: false, + ..Settings::default() + } + ); +} + +#[test] +fn file_zero_takes_one_arg() { + let settings = parse(&["shuf", "-z", "myfile"]).unwrap(); + assert_eq!( + settings, + Settings { + file: Some("myfile".into()), + zero: true, + ..Settings::default() + } + ); +} + +#[test] +fn file_zero_postfix_takes_one_arg() { + let settings = parse(&["shuf", "myfile", "-z"]).unwrap(); + assert_eq!( + settings, + Settings { + file: Some("myfile".into()), + zero: true, + ..Settings::default() + } + ); +} + +#[test] +fn file_zero_refuses_two_files() { + // FIXME: Check detected error + assert!(parse(&["shuf", "-z", "myfile", "otherfile"]).is_err()); +} + +#[test] +fn file_zero_refuses_three_files() { + // FIXME: Check detected error + assert!(parse(&["shuf", "-z", "myfile", "otherfile", "morefile"]).is_err()); +} + +#[test] +fn echo_onearg() { + let settings = parse(&["shuf", "-e", "hello"]).unwrap(); + assert_eq!( + settings, + Settings { + echo: true, + echo_args: vec!["hello".into()], + ..Settings::default() + } + ); +} + +#[test] +fn echo_onearg_postfix() { + let settings = parse(&["shuf", "hello", "-e"]).unwrap(); + assert_eq!( + settings, + Settings { + echo: true, + echo_args: vec!["hello".into()], + ..Settings::default() + } + ); +} + +#[test] +fn echo_twoarg() { + let settings = parse(&["shuf", "-e", "hello", "world"]).unwrap(); + assert_eq!( + settings, + Settings { + echo: true, + echo_args: vec!["hello".into(), "world".into()], + ..Settings::default() + } + ); +} + +#[test] +fn echo_twoarg_postfix() { + let settings = parse(&["shuf", "hello", "world", "-e"]).unwrap(); + assert_eq!( + settings, + Settings { + echo: true, + echo_args: vec!["hello".into(), "world".into()], + ..Settings::default() + } + ); +} + +#[test] +fn echo_twoarg_infix() { + let settings = parse(&["shuf", "hello", "-e", "world"]).unwrap(); + assert_eq!( + settings, + Settings { + echo: true, + echo_args: vec!["hello".into(), "world".into()], + ..Settings::default() + } + ); +} + +#[test] +fn echo_onearg_zero_before() { + let settings = parse(&["shuf", "-z", "-e", "hello"]).unwrap(); + assert_eq!( + settings, + Settings { + echo: true, + echo_args: vec!["hello".into()], + zero: true, + ..Settings::default() + } + ); +} + +#[test] +fn echo_onearg_postfix_zero_before() { + let settings = parse(&["shuf", "-z", "hello", "-e"]).unwrap(); + assert_eq!( + settings, + Settings { + echo: true, + echo_args: vec!["hello".into()], + zero: true, + ..Settings::default() + } + ); +} + +#[test] +fn echo_twoarg_zero_before() { + let settings = parse(&["shuf", "-z", "-e", "hello", "world"]).unwrap(); + assert_eq!( + settings, + Settings { + echo: true, + echo_args: vec!["hello".into(), "world".into()], + zero: true, + ..Settings::default() + } + ); +} + +#[test] +fn echo_twoarg_postfix_zero_before() { + let settings = parse(&["shuf", "-z", "hello", "world", "-e"]).unwrap(); + assert_eq!( + settings, + Settings { + echo: true, + echo_args: vec!["hello".into(), "world".into()], + zero: true, + ..Settings::default() + } + ); +} + +#[test] +fn echo_twoarg_infix_zero_before() { + let settings = parse(&["shuf", "-z", "hello", "-e", "world"]).unwrap(); + assert_eq!( + settings, + Settings { + echo: true, + echo_args: vec!["hello".into(), "world".into()], + zero: true, + ..Settings::default() + } + ); +} + +#[test] +fn echo_onearg_zero_after() { + let settings = parse(&["shuf", "-e", "hello", "-z"]).unwrap(); + assert_eq!( + settings, + Settings { + echo: true, + echo_args: vec!["hello".into()], + zero: true, + ..Settings::default() + } + ); +} + +#[test] +fn echo_onearg_postfix_zero_after() { + let settings = parse(&["shuf", "hello", "-e", "-z"]).unwrap(); + assert_eq!( + settings, + Settings { + echo: true, + echo_args: vec!["hello".into()], + zero: true, + ..Settings::default() + } + ); +} + +#[test] +fn echo_twoarg_zero_after() { + let settings = parse(&["shuf", "-e", "hello", "world", "-z"]).unwrap(); + assert_eq!( + settings, + Settings { + echo: true, + echo_args: vec!["hello".into(), "world".into()], + zero: true, + ..Settings::default() + } + ); +} + +#[test] +fn echo_twoarg_postfix_zero_after() { + let settings = parse(&["shuf", "hello", "world", "-e", "-z"]).unwrap(); + assert_eq!( + settings, + Settings { + echo: true, + echo_args: vec!["hello".into(), "world".into()], + zero: true, + ..Settings::default() + } + ); +} + +#[test] +fn echo_twoarg_infix_zero_after() { + let settings = parse(&["shuf", "hello", "-e", "world", "-z"]).unwrap(); + assert_eq!( + settings, + Settings { + echo: true, + echo_args: vec!["hello".into(), "world".into()], + zero: true, + ..Settings::default() + } + ); +} + +#[test] +fn echo_collapse_zero_before_noarg() { + let settings = parse(&["shuf", "-ze"]).unwrap(); + assert_eq!( + settings, + Settings { + echo: true, + zero: true, + ..Settings::default() + } + ); +} + +#[test] +fn echo_collapse_zero_before_onearg() { + let settings = parse(&["shuf", "-ze", "hello"]).unwrap(); + assert_eq!( + settings, + Settings { + echo: true, + echo_args: vec!["hello".into()], + zero: true, + ..Settings::default() + } + ); +} + +#[test] +fn echo_collapse_zero_after_noarg() { + let settings = parse(&["shuf", "-ez"]).unwrap(); + assert_eq!( + settings, + Settings { + echo: true, + zero: true, + ..Settings::default() + } + ); +} + +#[test] +fn echo_collapse_zero_after_onearg() { + let settings = parse(&["shuf", "-ez", "hello"]).unwrap(); + assert_eq!( + settings, + Settings { + echo: true, + echo_args: vec!["hello".into()], + zero: true, + ..Settings::default() + } + ); +} From d62971d39aa6d53cfa7dc54b7287773dd161dbac Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 09:41:19 +0000 Subject: [PATCH 083/116] fix(deps): update rust crate syn to 2.0.51 --- derive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 878d589..d9d8d9f 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -11,4 +11,4 @@ proc_macro = true [dependencies] proc-macro2 = "1.0.78" quote = "1.0.35" -syn = { version = "2.0.50", features = ["full"] } +syn = { version = "2.0.51", features = ["full"] } From 975e8c13e82d9d6bd4c2c6070f397eef5667fc9a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 28 Feb 2024 22:04:56 +0000 Subject: [PATCH 084/116] fix(deps): update rust crate syn to 2.0.52 --- derive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index d9d8d9f..767a3b8 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -11,4 +11,4 @@ proc_macro = true [dependencies] proc-macro2 = "1.0.78" quote = "1.0.35" -syn = { version = "2.0.51", features = ["full"] } +syn = { version = "2.0.52", features = ["full"] } From 00a701e5afb8adcb146142ea47ea9b9902391a71 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 12 Mar 2024 06:00:50 +0000 Subject: [PATCH 085/116] fix(deps): update rust crate proc-macro2 to 1.0.79 --- derive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 767a3b8..0aa37c3 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -9,6 +9,6 @@ edition = "2021" proc_macro = true [dependencies] -proc-macro2 = "1.0.78" +proc-macro2 = "1.0.79" quote = "1.0.35" syn = { version = "2.0.52", features = ["full"] } From e33a66bc560e884dee5c63e43530f4557510d16b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 17 Mar 2024 01:41:35 +0000 Subject: [PATCH 086/116] fix(deps): update rust crate syn to 2.0.53 --- derive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 0aa37c3..97fd45a 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -11,4 +11,4 @@ proc_macro = true [dependencies] proc-macro2 = "1.0.79" quote = "1.0.35" -syn = { version = "2.0.52", features = ["full"] } +syn = { version = "2.0.53", features = ["full"] } From 207e291f55f69f8fe9238a840cde4ce167958df5 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Fri, 22 Mar 2024 23:32:24 +0100 Subject: [PATCH 087/116] implement Debug for ErrorKind --- src/error.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/error.rs b/src/error.rs index 0da36e5..2c168fa 100644 --- a/src/error.rs +++ b/src/error.rs @@ -66,7 +66,7 @@ impl StdError for Error {} impl Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.kind.fmt(f) + std::fmt::Display::fmt(&self.kind, f) } } @@ -76,6 +76,12 @@ impl Debug for Error { } } +impl Debug for ErrorKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self, f) + } +} + impl Display for ErrorKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "error: ")?; From 20417509c423290cd0065f42608010fc71bc1e2b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Apr 2024 22:56:36 +0000 Subject: [PATCH 088/116] fix(deps): update rust crate strsim to 0.11.1 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index c815db7..660520f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ readme = "README.md" [dependencies] uutils-args-derive = { version = "0.1.0", path = "derive" } uutils-args-complete = { version = "0.1.0", path = "complete", optional = true } -strsim = "0.11.0" +strsim = "0.11.1" lexopt = "0.3.0" [features] From b4e8a0566ff8b2c629e21a164ff8a8c2c70d594a Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Wed, 10 Apr 2024 10:18:28 +0200 Subject: [PATCH 089/116] Allow dead code in completion example --- examples/completion.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/completion.rs b/examples/completion.rs index 112cc60..d68e372 100644 --- a/examples/completion.rs +++ b/examples/completion.rs @@ -1,3 +1,4 @@ +#![allow(dead_code)] use std::path::PathBuf; use uutils_args::{Arguments, Options, Value}; From c6c24704c960084dda9afe5393ef8095bcf2e6e2 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Wed, 10 Apr 2024 10:33:49 +0200 Subject: [PATCH 090/116] Fix "field is never read" warnings --- tests/coreutils/dd.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/coreutils/dd.rs b/tests/coreutils/dd.rs index fe5ef69..8c938d8 100644 --- a/tests/coreutils/dd.rs +++ b/tests/coreutils/dd.rs @@ -102,14 +102,14 @@ impl Options for Settings { self.ibs = b; self.obs = b; } - Arg::Cbs(_) => todo!(), + Arg::Cbs(_b) => todo!(), Arg::Skip(b) => self.skip = b, Arg::Seek(b) => self.seek = b, Arg::Count(n) => self.count = n, Arg::Status(level) => self.status = Some(level), - Arg::Conv(_) => todo!(), - Arg::Iflag(_) => todo!(), - Arg::Oflag(_) => todo!(), + Arg::Conv(_c) => todo!(), + Arg::Iflag(_f) => todo!(), + Arg::Oflag(_f) => todo!(), } } } From 850f78d2c3253a37d409c8092e85208ce5b8ea2a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 14 Apr 2024 22:04:13 +0000 Subject: [PATCH 091/116] fix(deps): update rust crate proc-macro2 to 1.0.80 --- derive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 97fd45a..55072e6 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -9,6 +9,6 @@ edition = "2021" proc_macro = true [dependencies] -proc-macro2 = "1.0.79" +proc-macro2 = "1.0.80" quote = "1.0.35" syn = { version = "2.0.53", features = ["full"] } From ff1ea79461e00f28e7c22c5047b714e2f220b45b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 15 Apr 2024 05:08:15 +0000 Subject: [PATCH 092/116] fix(deps): update rust crate quote to 1.0.36 --- derive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 55072e6..bd2fcc7 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -10,5 +10,5 @@ proc_macro = true [dependencies] proc-macro2 = "1.0.80" -quote = "1.0.35" +quote = "1.0.36" syn = { version = "2.0.53", features = ["full"] } From 84cf44146abfae91ca282127d31d6d2a6453b219 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 15 Apr 2024 05:11:15 +0000 Subject: [PATCH 093/116] fix(deps): update rust crate syn to 2.0.59 --- derive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index bd2fcc7..437ea38 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -11,4 +11,4 @@ proc_macro = true [dependencies] proc-macro2 = "1.0.80" quote = "1.0.36" -syn = { version = "2.0.53", features = ["full"] } +syn = { version = "2.0.59", features = ["full"] } From 18b475c9015f24b6e6589938df3e3263d2ed6027 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 17 Apr 2024 02:18:06 +0000 Subject: [PATCH 094/116] fix(deps): update rust crate proc-macro2 to 1.0.81 --- derive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 437ea38..df4c376 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -9,6 +9,6 @@ edition = "2021" proc_macro = true [dependencies] -proc-macro2 = "1.0.80" +proc-macro2 = "1.0.81" quote = "1.0.36" syn = { version = "2.0.59", features = ["full"] } From 1d0a41d4b57dd57da37dbc6fb689f7617d6474e0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 17 Apr 2024 22:16:38 +0000 Subject: [PATCH 095/116] fix(deps): update rust crate syn to 2.0.60 --- derive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index df4c376..5ce3d7b 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -11,4 +11,4 @@ proc_macro = true [dependencies] proc-macro2 = "1.0.81" quote = "1.0.36" -syn = { version = "2.0.59", features = ["full"] } +syn = { version = "2.0.60", features = ["full"] } From 0081703757ac01f6ac599dcca316c958bf9adda8 Mon Sep 17 00:00:00 2001 From: Ben Wiederhake Date: Mon, 6 May 2024 03:23:20 +0200 Subject: [PATCH 096/116] Implement and exhaustively test cksum's weird implicit flags --- tests/coreutils.rs | 3 + tests/coreutils/cksum.rs | 201 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 tests/coreutils/cksum.rs diff --git a/tests/coreutils.rs b/tests/coreutils.rs index 6ebfeac..de16e8d 100644 --- a/tests/coreutils.rs +++ b/tests/coreutils.rs @@ -13,6 +13,9 @@ mod basename; #[path = "coreutils/cat.rs"] mod cat; +#[path = "coreutils/cksum.rs"] +mod cksum; + #[path = "coreutils/dd.rs"] mod dd; diff --git a/tests/coreutils/cksum.rs b/tests/coreutils/cksum.rs new file mode 100644 index 0000000..71682fd --- /dev/null +++ b/tests/coreutils/cksum.rs @@ -0,0 +1,201 @@ +use uutils_args::{Arguments, Options}; + +#[derive(Debug, Clone, Arguments)] +enum Arg { + #[arg("-b", "--binary")] + Binary, + + #[arg("-t", "--text")] + Text, + + #[arg("--tag")] + Tag, + + #[arg("--untagged")] + Untagged, +} + +#[derive(Default, Debug, PartialEq)] +enum Tristate { + True, + #[default] + Unset, + False, +} + +#[derive(Default, Debug)] +struct Settings { + binary: Tristate, + tag: Tristate, +} + +impl Options for Settings { + fn apply(&mut self, arg: Arg) { + match arg { + Arg::Binary => self.binary = Tristate::True, + Arg::Text => self.binary = Tristate::False, + Arg::Tag => { + // https://github.com/uutils/coreutils/issues/6364 + self.binary = Tristate::Unset; + self.tag = Tristate::True; + } + Arg::Untagged => { + // https://github.com/uutils/coreutils/issues/6364 + if self.tag == Tristate::True { + self.binary = Tristate::Unset; + } + self.tag = Tristate::False; + } + } + } +} + +#[derive(Debug, PartialEq)] +enum ResultingFormat { + UntaggedText, + UntaggedBinary, + Tagged, + ErrorInstead, +} + +impl Settings { + fn format(&self) -> ResultingFormat { + // Interpret "Unset" as "tagged": + if self.tag != Tristate::False { + // -> Tagged. + // Error only if the user explicitly requests the text format: + if self.binary == Tristate::False { + ResultingFormat::ErrorInstead + } else { + ResultingFormat::Tagged + } + } else { + // -> Untagged. + // Binary only if the user explicitly requests it: + if self.binary == Tristate::True { + ResultingFormat::UntaggedBinary + } else { + ResultingFormat::UntaggedText + } + } + } +} + +// Convenience function for testing +#[cfg(test)] +fn assert_format(args: &[&str], expected: ResultingFormat) { + let mut full_argv = vec!["bin_name"]; + full_argv.extend(args); + let result = Settings::default().parse(full_argv).unwrap(); + assert_eq!( + (result.0.format(), result.1.as_slice()), + (expected, [].as_slice()), + "{:?}", + args + ); +} + +// These tests basically force the reader to make the same conclusions as +// https://github.com/uutils/coreutils/issues/6364 +// Quotes from the issue are marked with a leading ">". + +#[test] +fn binary_text_toggle_in_tagged() { + // > Observe that -b/-t seems to be doing precisely what we would hope for: toggle between binary/text mode: + // -b/-t/--tagged switch between tagged/error behavior + assert_format(&[], ResultingFormat::Tagged); + assert_format(&["-t"], ResultingFormat::ErrorInstead); + assert_format(&["-t", "-b"], ResultingFormat::Tagged); + assert_format(&["-t", "--tag"], ResultingFormat::Tagged); +} + +#[test] +fn binary_text_toggle_in_untagged() { + // Once we're in untagged format, -b/-t switch between binary/text behavior + assert_format(&["--untagged"], ResultingFormat::UntaggedText); + assert_format(&["--untagged", "-t"], ResultingFormat::UntaggedText); + assert_format(&["--untagged", "-b"], ResultingFormat::UntaggedBinary); + assert_format(&["--untagged", "-t", "-b"], ResultingFormat::UntaggedBinary); + assert_format(&["--untagged", "-b", "-t"], ResultingFormat::UntaggedText); +} + +// > Observe that --tag/--untagged seems to be the flags that have the weird behavior attached to +// > them. In particular, the T state seems to be more that one actual state, probably +// > differentiated along the "text-binary-axis". + +#[test] +fn nondeterministic_edges() { + // Same behavior: + assert_format(&[], ResultingFormat::Tagged); + assert_format(&["-b"], ResultingFormat::Tagged); + // But must have different internal state: + assert_format(&["--untagged"], ResultingFormat::UntaggedText); + assert_format(&["-b", "--untagged"], ResultingFormat::UntaggedBinary); +} + +#[test] +fn selfloops() { + // "T" + assert_format(&[], ResultingFormat::Tagged); + assert_format(&["-b"], ResultingFormat::Tagged); + assert_format(&["--tag"], ResultingFormat::Tagged); + assert_format(&["-b", "--tag"], ResultingFormat::Tagged); + // "E" + assert_format(&["-t"], ResultingFormat::ErrorInstead); + assert_format(&["-t", "-t"], ResultingFormat::ErrorInstead); + // "A" + assert_format(&["-b", "--untagged"], ResultingFormat::UntaggedBinary); + assert_format(&["-b", "--untagged", "-b"], ResultingFormat::UntaggedBinary); + assert_format( + &["-b", "--untagged", "--untagged"], + ResultingFormat::UntaggedBinary, + ); + // "S" + assert_format(&["--untagged"], ResultingFormat::UntaggedText); + assert_format(&["--untagged", "-t"], ResultingFormat::UntaggedText); + assert_format(&["--untagged", "--untagged"], ResultingFormat::UntaggedText); +} + +#[test] +fn other_diagonals() { + // From "A" and "S" ... + assert_format(&["-b", "--untagged"], ResultingFormat::UntaggedBinary); + assert_format(&["--untagged"], ResultingFormat::UntaggedText); + // ... to "T": + assert_format(&["-b", "--untagged", "--tag"], ResultingFormat::Tagged); + assert_format(&["--untagged", "--tag"], ResultingFormat::Tagged); + // From "E" to "S": + assert_format(&["-t"], ResultingFormat::ErrorInstead); + assert_format(&["-t", "--untagged"], ResultingFormat::UntaggedText); +} + +#[test] +fn suffix_b_u_not_deterministic() { + // > Ending in bU does not determine the result: + assert_format(&["-b", "--untagged"], ResultingFormat::UntaggedBinary); + assert_format( + &["--tag", "-b", "--untagged"], + ResultingFormat::UntaggedText, + ); + assert_format( + &["--untagged", "-b", "--untagged"], + ResultingFormat::UntaggedBinary, + ); + assert_format( + &["-b", "--untagged", "-b", "--untagged"], + ResultingFormat::UntaggedBinary, + ); + assert_format( + &["--tag", "--untagged", "-b", "--untagged"], + ResultingFormat::UntaggedBinary, + ); + assert_format( + &["--untagged", "--tag", "-b", "--untagged"], + ResultingFormat::UntaggedText, + ); + // > Therefore, U does not set the binary-ness to a constant, but rather depends on the tagged-ness. +} + +// I *think* that this battery of tests fully specifies the full behavior. +// In any case, brute-forcing all of the 4^n combinations up to 5 arguments +// shows no counter-examples, so this implementation is definitely a good match. From df7bc4e2d9a8a9ce4039bb2e90665a29cc4f5d4d Mon Sep 17 00:00:00 2001 From: Ben Wiederhake Date: Thu, 9 May 2024 21:02:30 +0200 Subject: [PATCH 097/116] fix nightly clippy warnings Note that this still compiles warning-free with rustc 1.70. --- derive/Cargo.toml | 2 +- derive/src/argument.rs | 3 --- tests/options.rs | 3 ++- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 5ce3d7b..1e6b1a3 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] -proc_macro = true +proc-macro = true [dependencies] proc-macro2 = "1.0.81" diff --git a/derive/src/argument.rs b/derive/src/argument.rs index ddb4a5a..dec13c2 100644 --- a/derive/src/argument.rs +++ b/derive/src/argument.rs @@ -13,7 +13,6 @@ use crate::{ pub struct Argument { pub ident: Ident, pub field: Option, - pub name: String, pub arg_type: ArgType, pub help: String, } @@ -41,7 +40,6 @@ pub fn parse_arguments_attr(attrs: &[Attribute]) -> ArgumentsAttr { pub fn parse_argument(v: Variant) -> Vec { let ident = v.ident; - let name = ident.to_string(); let attributes = get_arg_attributes(&v.attrs).unwrap(); // Return early because we don't need to check the fields if it's not used. @@ -94,7 +92,6 @@ pub fn parse_argument(v: Variant) -> Vec { Argument { ident: ident.clone(), field: field.clone(), - name: name.clone(), arg_type, help: arg_help, } diff --git a/tests/options.rs b/tests/options.rs index 1f1e972..2475db0 100644 --- a/tests/options.rs +++ b/tests/options.rs @@ -286,7 +286,7 @@ fn actions() { fn apply(&mut self, arg: Arg) { match arg { Arg::Message(m) => { - self.last_message = m.clone(); + self.last_message.clone_from(&m); self.messages.push(m); } Arg::Send => self.send = true, @@ -617,6 +617,7 @@ fn empty_value() { Val(V), } + #[allow(dead_code)] struct Settings {} impl Options for Settings { From 2cdf904daf5d01477857dadd5bd51884d474a551 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Fri, 10 Jan 2025 15:00:06 +0100 Subject: [PATCH 098/116] clippy: fix warning from unnecessary_map_or lint --- derive/src/help_parser.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/src/help_parser.rs b/derive/src/help_parser.rs index 3d9e5ae..16a0dba 100644 --- a/derive/src/help_parser.rs +++ b/derive/src/help_parser.rs @@ -71,7 +71,7 @@ pub fn parse_usage(content: &str) -> String { pub fn parse_section(section: &str, content: &str) -> Option { fn is_section_header(line: &str, section: &str) -> bool { line.strip_prefix("##") - .map_or(false, |l| l.trim().to_lowercase() == section) + .is_some_and(|l| l.trim().to_lowercase() == section) } let section = §ion.to_lowercase(); From dca1347b743653c23e42f56d648b81e018bde1f3 Mon Sep 17 00:00:00 2001 From: Ben Wiederhake Date: Fri, 29 Mar 2024 03:32:30 +0100 Subject: [PATCH 099/116] make Options::apply fallible --- README.md | 3 ++- docs/guide/port.md | 17 +++++++++------ docs/guide/quick.md | 20 +++++++++++------- examples/completion.rs | 2 +- examples/deprecated.rs | 3 ++- examples/hello_world.rs | 3 ++- examples/value.rs | 3 ++- src/lib.rs | 4 ++-- tests/coreutils/b2sum.rs | 3 ++- tests/coreutils/base32.rs | 3 ++- tests/coreutils/basename.rs | 3 ++- tests/coreutils/cat.rs | 3 ++- tests/coreutils/cksum.rs | 3 ++- tests/coreutils/dd.rs | 3 ++- tests/coreutils/echo.rs | 3 ++- tests/coreutils/head.rs | 3 ++- tests/coreutils/ls.rs | 3 ++- tests/coreutils/mktemp.rs | 3 ++- tests/coreutils/shuf.rs | 3 ++- tests/coreutils/tail.rs | 3 ++- tests/coreutils/uniq.rs | 5 +++-- tests/flags.rs | 41 +++++++++++++++++++++++------------- tests/options.rs | 42 ++++++++++++++++++++++++------------- tests/options_first.rs | 3 ++- 24 files changed, 118 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index d27b71e..15801e5 100644 --- a/README.md +++ b/README.md @@ -67,11 +67,12 @@ struct Settings { // To implement `Options`, we only need to provide the `apply` method. // The `parse` method will be automatically generated. impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { match arg { Arg::Caps => self.caps = true, Arg::ExclamationMarks(n) => self.exclamation_marks += n, } + Ok(()) } } diff --git a/docs/guide/port.md b/docs/guide/port.md index f7d6070..990f59b 100644 --- a/docs/guide/port.md +++ b/docs/guide/port.md @@ -94,10 +94,11 @@ enum Arg { struct Settings { a: bool } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { match arg { Arg::A => self.a = true, } + Ok(()) } } @@ -137,10 +138,11 @@ impl Default for Settings { } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { match arg { Arg::A => self.a = false, } + Ok(()) } } @@ -175,10 +177,11 @@ enum Arg { struct Settings { a: u8 } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { match arg { Arg::A => self.a += 1, } + Ok(()) } } @@ -215,10 +218,11 @@ enum Arg { struct Settings { a: OsString } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { match arg { Arg::A(s) => self.a = s, } + Ok(()) } } @@ -255,10 +259,11 @@ enum Arg { struct Settings { a: Vec } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { match arg { Arg::A(s) => self.a.push(s), } + Ok(()) } } @@ -271,4 +276,4 @@ let a = Settings::default().parse(std::env::args_os()).unwrap().0.a; [Up](super) [Next](next) -
\ No newline at end of file +
diff --git a/docs/guide/quick.md b/docs/guide/quick.md index 31f515b..5de6afb 100644 --- a/docs/guide/quick.md +++ b/docs/guide/quick.md @@ -59,10 +59,11 @@ struct Settings { } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { match arg { Arg::Force => self.force = true, } + Ok(()) } } @@ -100,11 +101,12 @@ struct Settings { } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { match arg { Arg::Force => self.force = true, Arg::NoForce => self.force = false, } + Ok(()) } } @@ -160,10 +162,11 @@ enum Arg { # } # # impl Options for Settings { -# fn apply(&mut self, arg: Arg) { +# fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { # match arg { # Arg::Name(name) => self.name = name, # } +# Ok(()) # } # } # @@ -197,10 +200,11 @@ enum Arg { # } # # impl Options for Settings { -# fn apply(&mut self, arg: Arg) { +# fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { # match arg { # Arg::Name(name) => self.name = name, # } +# Ok(()) # } # } # @@ -234,10 +238,11 @@ enum Arg { # } # # impl Options for Settings { -# fn apply(&mut self, arg: Arg) { +# fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { # match arg { # Arg::Force(b) => self.force = b, # } +# Ok(()) # } # } # @@ -269,10 +274,11 @@ enum Arg { # } # # impl Options for Settings { -# fn apply(&mut self, arg: Arg) { +# fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { # match arg { # Arg::Sort(s) => self.sort = s, # } +# Ok(()) # } # } # @@ -287,4 +293,4 @@ enum Arg { [Up](super) [Next](next) - \ No newline at end of file + diff --git a/examples/completion.rs b/examples/completion.rs index d68e372..bcfda9a 100644 --- a/examples/completion.rs +++ b/examples/completion.rs @@ -33,7 +33,7 @@ enum Arg { struct Settings; impl Options for Settings { - fn apply(&mut self, _arg: Arg) { + fn apply(&mut self, _arg: Arg) -> Result<(), uutils_args::Error> { panic!("Compile with the 'parse-is-complete' feature!") } } diff --git a/examples/deprecated.rs b/examples/deprecated.rs index 2a6d5bd..71db541 100644 --- a/examples/deprecated.rs +++ b/examples/deprecated.rs @@ -34,11 +34,12 @@ struct Settings { } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { match arg { Arg::Min(n) => self.n1 = n, Arg::Plus(n) => self.n2 = n, } + Ok(()) } } diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 7bd4eb0..8df6742 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -22,12 +22,13 @@ struct Settings { } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + 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::Hidden => {} } + Ok(()) } } diff --git a/examples/value.rs b/examples/value.rs index 5359192..67926a1 100644 --- a/examples/value.rs +++ b/examples/value.rs @@ -25,10 +25,11 @@ struct Settings { } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { match arg { Arg::Color(c) => self.color = c, } + Ok(()) } } diff --git a/src/lib.rs b/src/lib.rs index 5164734..e7b7058 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -166,7 +166,7 @@ impl ArgumentIter { /// call [`Options::apply`] on the result until the arguments are exhausted. pub trait Options: Sized { /// Apply a single argument to the options. - fn apply(&mut self, arg: Arg); + fn apply(&mut self, arg: Arg) -> Result<(), Error>; /// Parse an iterator of arguments into the options #[allow(unused_mut)] @@ -191,7 +191,7 @@ pub trait Options: Sized { { let mut iter = ArgumentIter::::from_args(args); while let Some(arg) = iter.next_arg()? { - self.apply(arg); + self.apply(arg)?; } Ok((self, iter.positional_arguments)) } diff --git a/tests/coreutils/b2sum.rs b/tests/coreutils/b2sum.rs index 3f24c71..a2081e3 100644 --- a/tests/coreutils/b2sum.rs +++ b/tests/coreutils/b2sum.rs @@ -46,7 +46,7 @@ struct Settings { } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { match arg { Arg::Binary => self.binary = true, Arg::Check => self.check = true, @@ -57,6 +57,7 @@ impl Options for Settings { Arg::Strict => self.strict = true, Arg::Warn => self.check_output = CheckOutput::Warn, } + Ok(()) } } diff --git a/tests/coreutils/base32.rs b/tests/coreutils/base32.rs index 24ce251..e44ad9a 100644 --- a/tests/coreutils/base32.rs +++ b/tests/coreutils/base32.rs @@ -34,13 +34,14 @@ impl Default for Settings { } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { match arg { Arg::Decode => self.decode = true, Arg::IgnoreGarbage => self.ignore_garbage = true, Arg::Wrap(0) => self.wrap = None, Arg::Wrap(x) => self.wrap = Some(x), } + Ok(()) } } diff --git a/tests/coreutils/basename.rs b/tests/coreutils/basename.rs index 9324fe3..473610e 100644 --- a/tests/coreutils/basename.rs +++ b/tests/coreutils/basename.rs @@ -26,7 +26,7 @@ struct Settings { } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { match arg { Arg::Multiple => self.multiple = true, Arg::Suffix(s) => { @@ -35,6 +35,7 @@ impl Options for Settings { } Arg::Zero => self.zero = true, } + Ok(()) } } diff --git a/tests/coreutils/cat.rs b/tests/coreutils/cat.rs index 03df331..8b8da96 100644 --- a/tests/coreutils/cat.rs +++ b/tests/coreutils/cat.rs @@ -48,7 +48,7 @@ struct Settings { } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { match arg { Arg::ShowAll => { self.show_tabs = true; @@ -70,6 +70,7 @@ impl Options for Settings { Arg::NumberNonblank => self.number = NumberingMode::NonEmpty, Arg::SqueezeBlank => self.squeeze_blank = true, } + Ok(()) } } diff --git a/tests/coreutils/cksum.rs b/tests/coreutils/cksum.rs index 71682fd..b6a8086 100644 --- a/tests/coreutils/cksum.rs +++ b/tests/coreutils/cksum.rs @@ -30,7 +30,7 @@ struct Settings { } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { match arg { Arg::Binary => self.binary = Tristate::True, Arg::Text => self.binary = Tristate::False, @@ -47,6 +47,7 @@ impl Options for Settings { self.tag = Tristate::False; } } + Ok(()) } } diff --git a/tests/coreutils/dd.rs b/tests/coreutils/dd.rs index 8c938d8..b8b5358 100644 --- a/tests/coreutils/dd.rs +++ b/tests/coreutils/dd.rs @@ -92,7 +92,7 @@ impl Default for Settings { } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { match arg { Arg::Infile(f) => self.infile = Some(f), Arg::Outfile(f) => self.outfile = Some(f), @@ -111,6 +111,7 @@ impl Options for Settings { Arg::Iflag(_f) => todo!(), Arg::Oflag(_f) => todo!(), } + Ok(()) } } diff --git a/tests/coreutils/echo.rs b/tests/coreutils/echo.rs index 2d561f2..89c3fd7 100644 --- a/tests/coreutils/echo.rs +++ b/tests/coreutils/echo.rs @@ -24,12 +24,13 @@ struct Settings { } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { match arg { Arg::NoNewline => self.trailing_newline = false, Arg::EnableEscape => self.escape = true, Arg::DisableEscape => self.escape = false, } + Ok(()) } } diff --git a/tests/coreutils/head.rs b/tests/coreutils/head.rs index cdf8326..90d6375 100644 --- a/tests/coreutils/head.rs +++ b/tests/coreutils/head.rs @@ -188,7 +188,7 @@ struct Settings { } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { match arg { Arg::Bytes(n) => { self.mode = Mode::Bytes; @@ -202,6 +202,7 @@ impl Options for Settings { Arg::Verbose => self.verbose = true, Arg::Zero => self.zero = true, } + Ok(()) } } diff --git a/tests/coreutils/ls.rs b/tests/coreutils/ls.rs index 9673926..4eca547 100644 --- a/tests/coreutils/ls.rs +++ b/tests/coreutils/ls.rs @@ -360,7 +360,7 @@ impl Default for Settings { } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { match arg { Arg::All => self.which_files = Files::All, Arg::AlmostAll => self.which_files = Files::AlmostAll, @@ -417,6 +417,7 @@ impl Options for Settings { } Arg::GroupDirectoriesFirst => self.group_directories_first = true, } + Ok(()) } } diff --git a/tests/coreutils/mktemp.rs b/tests/coreutils/mktemp.rs index 2410f7d..fb5608f 100644 --- a/tests/coreutils/mktemp.rs +++ b/tests/coreutils/mktemp.rs @@ -40,7 +40,7 @@ struct Settings { } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { match arg { Arg::Directory => self.directory = true, Arg::DryRun => self.dry_run = true, @@ -49,6 +49,7 @@ impl Options for Settings { Arg::TreatAsTemplate => self.treat_as_template = true, Arg::TmpDir(dir) => self.tmp_dir = Some(dir), } + Ok(()) } } diff --git a/tests/coreutils/shuf.rs b/tests/coreutils/shuf.rs index 185e0b2..04ff88b 100644 --- a/tests/coreutils/shuf.rs +++ b/tests/coreutils/shuf.rs @@ -23,11 +23,12 @@ struct Settings { } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { match arg { Arg::Echo => self.echo = true, Arg::Zero => self.zero = true, } + Ok(()) } } diff --git a/tests/coreutils/tail.rs b/tests/coreutils/tail.rs index 2c9573e..c302547 100644 --- a/tests/coreutils/tail.rs +++ b/tests/coreutils/tail.rs @@ -244,7 +244,7 @@ struct Settings { } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { match arg { Arg::Bytes(n) => { self.mode = Mode::Bytes; @@ -268,6 +268,7 @@ impl Options for Settings { Arg::Zero => self.zero = true, Arg::PresumeInputPipe => self.presume_input_pipe = true, } + Ok(()) } } diff --git a/tests/coreutils/uniq.rs b/tests/coreutils/uniq.rs index 28a3264..dbcce1d 100644 --- a/tests/coreutils/uniq.rs +++ b/tests/coreutils/uniq.rs @@ -65,7 +65,7 @@ struct Settings { } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { match arg { Arg::SkipFields(n) => { self.skip_fields = Some(n); @@ -100,6 +100,7 @@ impl Options for Settings { Arg::ZeroTerminated => { self.zero_terminated = true; } - } + }; + Ok(()) } } diff --git a/tests/flags.rs b/tests/flags.rs index c9877d1..b292c1b 100644 --- a/tests/flags.rs +++ b/tests/flags.rs @@ -14,10 +14,11 @@ fn one_flag() { } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { match arg { Arg::Foo => self.foo = true, } + Ok(()) } } @@ -42,11 +43,12 @@ fn two_flags() { } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { match arg { Arg::A => self.a = true, Arg::B => self.b = true, } + Ok(()) } } @@ -82,8 +84,9 @@ fn long_and_short_flag() { } impl Options for Settings { - fn apply(&mut self, Arg::Foo: Arg) { + fn apply(&mut self, Arg::Foo: Arg) -> Result<(), uutils_args::Error> { self.foo = true; + Ok(()) } } @@ -106,8 +109,9 @@ fn short_alias() { } impl Options for Settings { - fn apply(&mut self, Arg::Foo: Arg) { + fn apply(&mut self, Arg::Foo: Arg) -> Result<(), uutils_args::Error> { self.foo = true; + Ok(()) } } @@ -128,8 +132,9 @@ fn long_alias() { } impl Options for Settings { - fn apply(&mut self, Arg::Foo: Arg) { + fn apply(&mut self, Arg::Foo: Arg) -> Result<(), uutils_args::Error> { self.foo = true; + Ok(()) } } @@ -153,11 +158,12 @@ fn short_and_long_alias() { } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { match arg { Arg::Foo => self.foo = true, Arg::Bar => self.bar = true, } + Ok(()) } } @@ -209,7 +215,7 @@ fn xyz_map_to_abc() { } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { match arg { Arg::X => { self.a = true; @@ -225,6 +231,7 @@ fn xyz_map_to_abc() { self.c = true; } } + Ok(()) } } @@ -282,11 +289,12 @@ fn non_rust_ident() { } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { match arg { Arg::FooBar => self.a = true, Arg::Super => self.b = true, } + Ok(()) } } @@ -312,8 +320,9 @@ fn number_flag() { } impl Options for Settings { - fn apply(&mut self, Arg::One: Arg) { + fn apply(&mut self, Arg::One: Arg) -> Result<(), uutils_args::Error> { self.one = true; + Ok(()) } } @@ -336,11 +345,12 @@ fn false_bool() { } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { self.foo = match arg { Arg::A => true, Arg::B => false, - } + }; + Ok(()) } } @@ -378,8 +388,9 @@ fn verbosity() { } impl Options for Settings { - fn apply(&mut self, Arg::Verbosity: Arg) { + fn apply(&mut self, Arg::Verbosity: Arg) -> Result<(), uutils_args::Error> { self.verbosity += 1; + Ok(()) } } @@ -429,12 +440,13 @@ fn infer_long_args() { } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { match arg { Arg::All => self.all = true, Arg::AlmostAll => self.almost_all = true, Arg::Author => self.author = true, } + Ok(()) } } @@ -482,12 +494,13 @@ fn enum_flag() { } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { self.foo = match arg { Arg::Foo => SomeEnum::Foo, Arg::Bar => SomeEnum::Bar, Arg::Baz => SomeEnum::Baz, }; + Ok(()) } } diff --git a/tests/options.rs b/tests/options.rs index 2475db0..b9d2902 100644 --- a/tests/options.rs +++ b/tests/options.rs @@ -16,8 +16,9 @@ fn string_option() { } impl Options for Settings { - fn apply(&mut self, Arg::Message(s): Arg) { - self.message = s + fn apply(&mut self, Arg::Message(s): Arg) -> Result<(), uutils_args::Error> { + self.message = s; + Ok(()) } } @@ -56,8 +57,9 @@ fn enum_option() { } impl Options for Settings { - fn apply(&mut self, Arg::Format(f): Arg) { + fn apply(&mut self, Arg::Format(f): Arg) -> Result<(), uutils_args::Error> { self.format = f; + Ok(()) } } @@ -103,8 +105,9 @@ fn enum_option_with_fields() { } impl Options for Settings { - fn apply(&mut self, Arg::Indent(i): Arg) { + fn apply(&mut self, Arg::Indent(i): Arg) -> Result<(), uutils_args::Error> { self.indent = i; + Ok(()) } } @@ -160,8 +163,9 @@ fn enum_with_complex_from_value() { } impl Options for Settings { - fn apply(&mut self, Arg::Indent(i): Arg) { + fn apply(&mut self, Arg::Indent(i): Arg) -> Result<(), uutils_args::Error> { self.indent = i; + Ok(()) } } @@ -208,8 +212,9 @@ fn color() { } impl Options for Settings { - fn apply(&mut self, Arg::Color(c): Arg) { + fn apply(&mut self, Arg::Color(c): Arg) -> Result<(), uutils_args::Error> { self.color = c.unwrap_or(Color::Always); + Ok(()) } } @@ -283,7 +288,7 @@ fn actions() { } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { match arg { Arg::Message(m) => { self.last_message.clone_from(&m); @@ -292,6 +297,7 @@ fn actions() { Arg::Send => self.send = true, Arg::Receive => self.send = false, } + Ok(()) } } @@ -317,11 +323,12 @@ fn width() { } impl Options for Settings { - fn apply(&mut self, Arg::Width(w): Arg) { + fn apply(&mut self, Arg::Width(w): Arg) -> Result<(), uutils_args::Error> { self.width = match w { 0 => None, x => Some(x), - } + }; + Ok(()) } } @@ -367,7 +374,7 @@ fn integers() { } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { self.n = match arg { Arg::U8(x) => x as i128, Arg::U16(x) => x as i128, @@ -379,7 +386,8 @@ fn integers() { Arg::I32(x) => x as i128, Arg::I64(x) => x as i128, Arg::I128(x) => x, - } + }; + Ok(()) } } @@ -454,8 +462,9 @@ fn ls_classify() { } impl Options for Settings { - fn apply(&mut self, Arg::Classify(c): Arg) { + fn apply(&mut self, Arg::Classify(c): Arg) -> Result<(), uutils_args::Error> { self.classify = c; + Ok(()) } } @@ -507,8 +516,9 @@ fn mktemp_tmpdir() { } impl Options for Settings { - fn apply(&mut self, Arg::TmpDir(dir): Arg) { + fn apply(&mut self, Arg::TmpDir(dir): Arg) -> Result<(), uutils_args::Error> { self.tmpdir = Some(dir); + Ok(()) } } @@ -581,11 +591,12 @@ fn deprecated() { } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { match arg { Arg::Min(n) => self.n1 = n, Arg::Plus(n) => self.n2 = n, } + Ok(()) } } @@ -621,10 +632,11 @@ fn empty_value() { struct Settings {} impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { match arg { Arg::Val(_) => {} } + Ok(()) } } } diff --git a/tests/options_first.rs b/tests/options_first.rs index ee9bc73..c7fa0e7 100644 --- a/tests/options_first.rs +++ b/tests/options_first.rs @@ -18,10 +18,11 @@ fn timeout_like() { } impl Options for Settings { - fn apply(&mut self, arg: Arg) { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { match arg { Arg::Verbose => self.verbose = true, } + Ok(()) } } From 67373eb4df1e02c4934bb3af0efbe2cf98297557 Mon Sep 17 00:00:00 2001 From: Ben Wiederhake Date: Fri, 29 Mar 2024 03:51:00 +0100 Subject: [PATCH 100/116] document new feature and add example / test --- docs/design/problems_with_clap.md | 16 ++++++++++++--- tests/flags.rs | 33 +++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/docs/design/problems_with_clap.md b/docs/design/problems_with_clap.md index d3c8774..308b2ac 100644 --- a/docs/design/problems_with_clap.md +++ b/docs/design/problems_with_clap.md @@ -108,7 +108,7 @@ uutils and when we opened as issue for it, it was discarded. This makes sense from `clap`'s perspective, but it shows that the priorities between `clap` and uutils diverge. -## Problem 6: It's stringly typed +## Problem 7: It's stringly typed `clap`'s arguments are identified by strings. This leads to code like this: @@ -135,14 +135,14 @@ deal, but a bit annoying. Of course, we wouldn't have this problem if we were able to use the derive API. -## Problem 7: Reading help string from a file +## Problem 8: Reading help string from a file In `uutils` our help strings can get quite long. Therefore, we like to extract those to an external file. With `clap` this means that we need to do some custom preprocessing on this file to extract the information for the several pieces of the help string that `clap` supports. -## Problem 8: No markdown support +## Problem 9: No markdown support Granted, this is not really a problem, but more of a nice-to-have. We have online documentation for the utils, based on the help strings and these are @@ -150,6 +150,16 @@ rendered from markdown. Ideally, our argument parser supports markdown too, so that we can have nicely rendered help strings which have (roughly) the same appearance in the terminal and online. +## Problem 10: No position-dependent argument-error prioritization + +This is the question of which error to print if both `-A` and `-B` are given, +and both are individually an error somehow. In case of the GNU tools, only the +first error is printed, and then the program is aborted. + +This also is not really a problem, but since it can be reasonably easily +achieved by simply raising an error during argument application, this enables +matching more closely the exact behavior of the GNU tools. + ## Good things about `clap` Alright, enough problems. Let's praise `clap` a bit, because it's an excellent diff --git a/tests/flags.rs b/tests/flags.rs index b292c1b..8d4569b 100644 --- a/tests/flags.rs +++ b/tests/flags.rs @@ -517,3 +517,36 @@ fn enum_flag() { SomeEnum::Baz, ); } + +#[test] +fn simple_error() { + #[derive(Arguments)] + enum Arg { + #[arg("-f", "--foo")] + Foo, + } + + #[derive(Debug, Default)] + struct Settings {} + + impl Options for Settings { + fn apply(&mut self, _arg: Arg) -> Result<(), uutils_args::Error> { + Err(uutils_args::Error { + exit_code: 42, + kind: uutils_args::ErrorKind::UnexpectedArgument( + "This is an example error".to_owned(), + ), + }) + } + } + + let settings_or_error = Settings::default().parse(["test", "-f"]); + let the_error = settings_or_error.expect_err("should have propagated error"); + assert_eq!(the_error.exit_code, 42); + match the_error.kind { + uutils_args::ErrorKind::UnexpectedArgument(err_str) => { + assert_eq!(err_str, "This is an example error") + } + _ => panic!("wrong error kind: {:?}", the_error.kind), + } +} From 9bc178ae38cef063ae306b9c3ba14d2fc152744a Mon Sep 17 00:00:00 2001 From: Ben Wiederhake Date: Fri, 29 Mar 2024 05:34:38 +0100 Subject: [PATCH 101/116] shuf: skip test that would exit the test suite too early --- tests/coreutils/shuf.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/coreutils/shuf.rs b/tests/coreutils/shuf.rs index 04ff88b..ff44e94 100644 --- a/tests/coreutils/shuf.rs +++ b/tests/coreutils/shuf.rs @@ -87,6 +87,7 @@ fn noarg_is_file_zero() { } #[test] +#[ignore = "exits too early"] fn the_help() { let settings = parse(&["shuf", "--help"]).unwrap(); assert_eq!( From 03b8dc1dd794d1697c0148af17efdf5959988d42 Mon Sep 17 00:00:00 2001 From: Ben Wiederhake Date: Fri, 29 Mar 2024 05:35:35 +0100 Subject: [PATCH 102/116] write date output format logic with exhaustive tests Also, exhausting tests. --- tests/coreutils.rs | 3 + tests/coreutils/date.rs | 674 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 677 insertions(+) create mode 100644 tests/coreutils/date.rs diff --git a/tests/coreutils.rs b/tests/coreutils.rs index de16e8d..eca5376 100644 --- a/tests/coreutils.rs +++ b/tests/coreutils.rs @@ -16,6 +16,9 @@ mod cat; #[path = "coreutils/cksum.rs"] mod cksum; +#[path = "coreutils/date.rs"] +mod date; + #[path = "coreutils/dd.rs"] mod dd; diff --git a/tests/coreutils/date.rs b/tests/coreutils/date.rs new file mode 100644 index 0000000..001addf --- /dev/null +++ b/tests/coreutils/date.rs @@ -0,0 +1,674 @@ +use std::ffi::OsString; +use uutils_args::{Arguments, Options, Value}; + +// Note: "+%s"-style format options aren't covered here, but should be! + +// +%s +// -I[FMT], --iso-8601[=FMT] output date/time in ISO 8601 format. +// -R, --rfc-email output date and time in RFC 5322 format. +// --rfc-3339=FMT output date/time in RFC 3339 format. +// date, hours, minutes, seconds, ns +// date, seconds, ns + +#[derive(Default, Debug, PartialEq, Eq, Value)] +enum Iso8601Format { + #[default] + #[value("date")] + Date, + + #[value("hours")] + Hours, + + #[value("minutes")] + Minutes, + + #[value("seconds")] + Seconds, + + #[value("ns")] + Ns, +} + +#[derive(Debug, PartialEq, Eq, Value)] +enum Rfc3339Format { + #[value("date")] + Date, + + #[value("seconds")] + Seconds, + + #[value("ns")] + Ns, +} + +#[derive(Arguments)] +enum Arg { + #[arg("-I[FMT]")] + #[arg("--iso-8601[=FMT]")] + Iso(Iso8601Format), + + #[arg("--rfc-3339=FMT")] + Rfc3339(Rfc3339Format), + + #[arg("-R")] + #[arg("--rfc-email")] + RfcEmail, +} + +#[derive(Debug, Default, PartialEq, Eq)] +enum Format { + #[default] + Unspecified, + Iso8601(Iso8601Format), + Rfc3339(Rfc3339Format), + RfcEmail, + // FromString(OsString), +} + +#[derive(Debug, Default, PartialEq, Eq)] +struct Settings { + chosen_format: Format, +} + +const MAGIC_MULTI_OUTPUT_ARG: &str = "! multiformat"; + +impl Options for Settings { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { + if self.chosen_format != Format::Unspecified { + return Err(uutils_args::Error { + exit_code: 1, + kind: uutils_args::ErrorKind::UnexpectedArgument(MAGIC_MULTI_OUTPUT_ARG.to_owned()), + }); + } + match arg { + Arg::Iso(iso) => self.chosen_format = Format::Iso8601(iso), + Arg::Rfc3339(rfc3339) => self.chosen_format = Format::Rfc3339(rfc3339), + Arg::RfcEmail => self.chosen_format = Format::RfcEmail, + } + Ok(()) + } +} + +#[test] +fn noarg() { + let (settings, operands) = Settings::default().parse(["date"]).unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!(settings.chosen_format, Format::Unspecified); +} + +#[test] +fn iso_short_noarg() { + let (settings, operands) = Settings::default().parse(["date", "-I"]).unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!(settings.chosen_format, Format::Iso8601(Iso8601Format::Date)); +} + +#[test] +fn iso_short_arg_direct_date() { + let (settings, operands) = Settings::default().parse(["date", "-Idate"]).unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!(settings.chosen_format, Format::Iso8601(Iso8601Format::Date)); +} + +#[test] +fn iso_short_arg_equal_date() { + // Not accepted by GNU, but we want to accept it. + let (settings, operands) = Settings::default().parse(["date", "-I=date"]).unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!(settings.chosen_format, Format::Iso8601(Iso8601Format::Date)); +} + +#[test] +fn iso_short_arg_space_date() { + let (settings, operands) = Settings::default().parse(["date", "-I", "date"]).unwrap(); + // Must not be interpreted as an argument to "-I". + assert_eq!(operands, vec!["date"]); + assert_eq!(settings.chosen_format, Format::Iso8601(Iso8601Format::Date)); +} + +#[test] +fn iso_short_arg_direct_minutes() { + let (settings, operands) = Settings::default().parse(["date", "-Iminutes"]).unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!( + settings.chosen_format, + Format::Iso8601(Iso8601Format::Minutes) + ); +} + +#[test] +fn iso_short_arg_equal_minutes() { + // Not accepted by GNU, but we want to accept it. + let (settings, operands) = Settings::default().parse(["date", "-I=minutes"]).unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!( + settings.chosen_format, + Format::Iso8601(Iso8601Format::Minutes) + ); +} + +#[test] +fn iso_short_arg_space_minutes() { + let (settings, operands) = Settings::default() + .parse(["date", "-I", "minutes"]) + .unwrap(); + // Must not be interpreted as an argument to "-I". + assert_eq!(operands, vec!["minutes"]); + assert_eq!(settings.chosen_format, Format::Iso8601(Iso8601Format::Date)); +} + +#[test] +fn iso_short_arg_invalid() { + let the_err = Settings::default() + .parse(["date", "-Idefinitely_invalid"]) + .unwrap_err(); + // Must not be interpreted as an argument to "-I". + assert_eq!(the_err.exit_code, 1); + match the_err.kind { + uutils_args::ErrorKind::ParsingFailed { option, value, .. } => { + assert_eq!(option, "-I"); + assert_eq!(value, "definitely_invalid"); + } + _ => panic!("wrong error kind: {:?}", the_err.kind), + } +} + +#[test] +fn iso_short_arg_equal_hours() { + let (settings, operands) = Settings::default().parse(["date", "-I=hours"]).unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!( + settings.chosen_format, + Format::Iso8601(Iso8601Format::Hours) + ); +} + +#[test] +fn iso_short_arg_equal_seconds() { + let (settings, operands) = Settings::default().parse(["date", "-I=seconds"]).unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!( + settings.chosen_format, + Format::Iso8601(Iso8601Format::Seconds) + ); +} + +#[test] +fn iso_short_arg_equal_ns() { + let (settings, operands) = Settings::default().parse(["date", "-I=ns"]).unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!(settings.chosen_format, Format::Iso8601(Iso8601Format::Ns)); +} + +#[test] +fn iso_short_arg_equal_hour_singular() { + let (settings, operands) = Settings::default().parse(["date", "-I=hour"]).unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!( + settings.chosen_format, + Format::Iso8601(Iso8601Format::Hours) + ); +} + +#[test] +fn iso_short_arg_equal_second_singular() { + let (settings, operands) = Settings::default().parse(["date", "-I=second"]).unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!( + settings.chosen_format, + Format::Iso8601(Iso8601Format::Seconds) + ); +} + +#[test] +fn iso_short_arg_equal_minute_singular() { + let (settings, operands) = Settings::default().parse(["date", "-I=minute"]).unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!( + settings.chosen_format, + Format::Iso8601(Iso8601Format::Minutes) + ); +} + +#[test] +fn iso_short_arg_equal_n_singular() { + let (settings, operands) = Settings::default().parse(["date", "-I=n"]).unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!(settings.chosen_format, Format::Iso8601(Iso8601Format::Ns)); +} + +#[test] +fn iso_long_noarg() { + let (settings, operands) = Settings::default().parse(["date", "--iso-8601"]).unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!(settings.chosen_format, Format::Iso8601(Iso8601Format::Date)); +} + +#[test] +fn iso_long_equal_date() { + let (settings, operands) = Settings::default() + .parse(["date", "--iso-8601=date"]) + .unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!(settings.chosen_format, Format::Iso8601(Iso8601Format::Date)); +} + +#[test] +fn iso_long_equal_hour() { + let (settings, operands) = Settings::default() + .parse(["date", "--iso-8601=hour"]) + .unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!( + settings.chosen_format, + Format::Iso8601(Iso8601Format::Hours) + ); +} + +#[test] +fn iso_long_space_hour() { + let (settings, operands) = Settings::default() + .parse(["date", "--iso-8601", "hour"]) + .unwrap(); + // Must not be interpreted as an argument to "-I". + assert_eq!(operands, vec!["hour"]); + assert_eq!(settings.chosen_format, Format::Iso8601(Iso8601Format::Date)); +} + +#[test] +fn iso_long_equal_n() { + let (settings, operands) = Settings::default().parse(["date", "--iso-8601=n"]).unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!(settings.chosen_format, Format::Iso8601(Iso8601Format::Ns)); +} + +#[test] +fn rfc3339_noarg() { + let the_err = Settings::default() + .parse(["date", "--rfc-3339"]) + .unwrap_err(); + assert_eq!(the_err.exit_code, 1); + match the_err.kind { + uutils_args::ErrorKind::MissingValue { option } => { + assert_eq!(option, Some("--rfc-3339".to_owned())); + } + _ => panic!("wrong error kind: {:?}", the_err.kind), + } +} + +#[test] +fn rfc3339_equal_date() { + let (settings, operands) = Settings::default() + .parse(["date", "--rfc-3339=date"]) + .unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!(settings.chosen_format, Format::Rfc3339(Rfc3339Format::Date)); +} + +#[test] +fn rfc3339_equal_ns() { + let (settings, operands) = Settings::default() + .parse(["date", "--rfc-3339=ns"]) + .unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!(settings.chosen_format, Format::Rfc3339(Rfc3339Format::Ns)); +} + +#[test] +fn rfc3339_equal_n_singular() { + let (settings, operands) = Settings::default().parse(["date", "--rfc-3339=n"]).unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!(settings.chosen_format, Format::Rfc3339(Rfc3339Format::Ns)); +} + +#[test] +fn rfc3339_equal_minutes() { + let the_err = Settings::default() + .parse(["date", "--rfc-3339=minutes"]) + .unwrap_err(); + assert_eq!(the_err.exit_code, 1); + match the_err.kind { + uutils_args::ErrorKind::ParsingFailed { option, value, .. } => { + assert_eq!(option, "--rfc-3339"); + assert_eq!(value, "minutes"); + } + _ => panic!("wrong error kind: {:?}", the_err.kind), + } +} + +#[test] +fn rfc3339_space_date() { + let (settings, operands) = Settings::default() + .parse(["date", "--rfc-3339", "date"]) + .unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!(settings.chosen_format, Format::Rfc3339(Rfc3339Format::Date)); +} + +#[test] +fn rfc3339_space_ns() { + let (settings, operands) = Settings::default() + .parse(["date", "--rfc-3339", "ns"]) + .unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!(settings.chosen_format, Format::Rfc3339(Rfc3339Format::Ns)); +} + +#[test] +fn rfc3339_space_n_singular() { + let (settings, operands) = Settings::default() + .parse(["date", "--rfc-3339", "n"]) + .unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!(settings.chosen_format, Format::Rfc3339(Rfc3339Format::Ns)); +} + +#[test] +fn rfc3339_space_minutes() { + let the_err = Settings::default() + .parse(["date", "--rfc-3339", "minutes"]) + .unwrap_err(); + assert_eq!(the_err.exit_code, 1); + match the_err.kind { + uutils_args::ErrorKind::ParsingFailed { option, value, .. } => { + assert_eq!(option, "--rfc-3339"); + assert_eq!(value, "minutes"); + } + _ => panic!("wrong error kind: {:?}", the_err.kind), + } +} + +#[test] +fn rfc_email_short() { + let (settings, operands) = Settings::default().parse(["date", "-R"]).unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!(settings.chosen_format, Format::RfcEmail); +} + +#[test] +fn rfc_email_long() { + let (settings, operands) = Settings::default().parse(["date", "--rfc-email"]).unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!(settings.chosen_format, Format::RfcEmail); +} + +#[test] +fn rfc_clash_isoshort_isoshort() { + let the_err = Settings::default().parse(["date", "-I", "-I"]).unwrap_err(); + assert_eq!(the_err.exit_code, 1); + match the_err.kind { + uutils_args::ErrorKind::UnexpectedArgument(arg) => { + assert_eq!(arg, MAGIC_MULTI_OUTPUT_ARG); + } + _ => panic!("wrong error kind: {:?}", the_err.kind), + } +} + +#[test] +fn rfc_clash_isoshort_isolong() { + let the_err = Settings::default() + .parse(["date", "-I", "--iso-8601"]) + .unwrap_err(); + assert_eq!(the_err.exit_code, 1); + match the_err.kind { + uutils_args::ErrorKind::UnexpectedArgument(arg) => { + assert_eq!(arg, MAGIC_MULTI_OUTPUT_ARG); + } + _ => panic!("wrong error kind: {:?}", the_err.kind), + } +} + +#[test] +fn rfc_clash_isoshort_rfc3339() { + let the_err = Settings::default() + .parse(["date", "-I", "--rfc-3339=date"]) + .unwrap_err(); + assert_eq!(the_err.exit_code, 1); + match the_err.kind { + uutils_args::ErrorKind::UnexpectedArgument(arg) => { + assert_eq!(arg, MAGIC_MULTI_OUTPUT_ARG); + } + _ => panic!("wrong error kind: {:?}", the_err.kind), + } +} + +#[test] +fn rfc_clash_isoshort_rfcemailshort() { + let the_err = Settings::default().parse(["date", "-I", "-R"]).unwrap_err(); + assert_eq!(the_err.exit_code, 1); + match the_err.kind { + uutils_args::ErrorKind::UnexpectedArgument(arg) => { + assert_eq!(arg, MAGIC_MULTI_OUTPUT_ARG); + } + _ => panic!("wrong error kind: {:?}", the_err.kind), + } +} + +#[test] +fn rfc_clash_isoshort_rfcemaillong() { + let the_err = Settings::default() + .parse(["date", "-I", "--rfc-email"]) + .unwrap_err(); + assert_eq!(the_err.exit_code, 1); + match the_err.kind { + uutils_args::ErrorKind::UnexpectedArgument(arg) => { + assert_eq!(arg, MAGIC_MULTI_OUTPUT_ARG); + } + _ => panic!("wrong error kind: {:?}", the_err.kind), + } +} + +#[test] +fn rfc_clash_isolong_isoshort() { + let the_err = Settings::default() + .parse(["date", "--iso-8601", "-I"]) + .unwrap_err(); + assert_eq!(the_err.exit_code, 1); + match the_err.kind { + uutils_args::ErrorKind::UnexpectedArgument(arg) => { + assert_eq!(arg, MAGIC_MULTI_OUTPUT_ARG); + } + _ => panic!("wrong error kind: {:?}", the_err.kind), + } +} + +#[test] +fn rfc_clash_isolong_isolong() { + let the_err = Settings::default() + .parse(["date", "--iso-8601", "--iso-8601"]) + .unwrap_err(); + assert_eq!(the_err.exit_code, 1); + match the_err.kind { + uutils_args::ErrorKind::UnexpectedArgument(arg) => { + assert_eq!(arg, MAGIC_MULTI_OUTPUT_ARG); + } + _ => panic!("wrong error kind: {:?}", the_err.kind), + } +} + +#[test] +fn rfc_clash_isolong_rfc3339() { + let the_err = Settings::default() + .parse(["date", "--iso-8601", "--rfc-3339=date"]) + .unwrap_err(); + assert_eq!(the_err.exit_code, 1); + match the_err.kind { + uutils_args::ErrorKind::UnexpectedArgument(arg) => { + assert_eq!(arg, MAGIC_MULTI_OUTPUT_ARG); + } + _ => panic!("wrong error kind: {:?}", the_err.kind), + } +} + +#[test] +fn rfc_clash_isolong_rfcemailshort() { + let the_err = Settings::default() + .parse(["date", "--iso-8601", "-R"]) + .unwrap_err(); + assert_eq!(the_err.exit_code, 1); + match the_err.kind { + uutils_args::ErrorKind::UnexpectedArgument(arg) => { + assert_eq!(arg, MAGIC_MULTI_OUTPUT_ARG); + } + _ => panic!("wrong error kind: {:?}", the_err.kind), + } +} + +#[test] +fn rfc_clash_isolong_rfcemaillong() { + let the_err = Settings::default() + .parse(["date", "--iso-8601", "--rfc-email"]) + .unwrap_err(); + assert_eq!(the_err.exit_code, 1); + match the_err.kind { + uutils_args::ErrorKind::UnexpectedArgument(arg) => { + assert_eq!(arg, MAGIC_MULTI_OUTPUT_ARG); + } + _ => panic!("wrong error kind: {:?}", the_err.kind), + } +} + +#[test] +fn rfc_clash_rfcemailshort_isoshort() { + let the_err = Settings::default().parse(["date", "-R", "-I"]).unwrap_err(); + assert_eq!(the_err.exit_code, 1); + match the_err.kind { + uutils_args::ErrorKind::UnexpectedArgument(arg) => { + assert_eq!(arg, MAGIC_MULTI_OUTPUT_ARG); + } + _ => panic!("wrong error kind: {:?}", the_err.kind), + } +} + +#[test] +fn rfc_clash_rfcemailshort_isolong() { + let the_err = Settings::default() + .parse(["date", "-R", "--iso-8601"]) + .unwrap_err(); + assert_eq!(the_err.exit_code, 1); + match the_err.kind { + uutils_args::ErrorKind::UnexpectedArgument(arg) => { + assert_eq!(arg, MAGIC_MULTI_OUTPUT_ARG); + } + _ => panic!("wrong error kind: {:?}", the_err.kind), + } +} + +#[test] +fn rfc_clash_rfcemailshort_rfc3339() { + let the_err = Settings::default() + .parse(["date", "-R", "--rfc-3339=date"]) + .unwrap_err(); + assert_eq!(the_err.exit_code, 1); + match the_err.kind { + uutils_args::ErrorKind::UnexpectedArgument(arg) => { + assert_eq!(arg, MAGIC_MULTI_OUTPUT_ARG); + } + _ => panic!("wrong error kind: {:?}", the_err.kind), + } +} + +#[test] +fn rfc_clash_rfcemailshort_rfcemailshort() { + let the_err = Settings::default().parse(["date", "-R", "-R"]).unwrap_err(); + assert_eq!(the_err.exit_code, 1); + match the_err.kind { + uutils_args::ErrorKind::UnexpectedArgument(arg) => { + assert_eq!(arg, MAGIC_MULTI_OUTPUT_ARG); + } + _ => panic!("wrong error kind: {:?}", the_err.kind), + } +} + +#[test] +fn rfc_clash_rfcemailshort_rfcemaillong() { + let the_err = Settings::default() + .parse(["date", "-R", "--rfc-email"]) + .unwrap_err(); + assert_eq!(the_err.exit_code, 1); + match the_err.kind { + uutils_args::ErrorKind::UnexpectedArgument(arg) => { + assert_eq!(arg, MAGIC_MULTI_OUTPUT_ARG); + } + _ => panic!("wrong error kind: {:?}", the_err.kind), + } +} + +#[test] +#[ignore = "exits too early, but works correctly"] +fn default_show_help() { + let (settings, operands) = Settings::default().parse(&["date", "--help"]).unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!(settings.chosen_format, Format::Unspecified); +} + +#[test] +#[ignore = "BROKEN, exits too early"] +fn rfcemail_show_help() { + let (settings, operands) = Settings::default() + .parse(&["date", "-R", "--help"]) + .unwrap(); + assert_eq!(operands, Vec::::new()); + assert_eq!(settings.chosen_format, Format::RfcEmail); +} + +#[test] +fn multi_output_has_priority() { + let the_err = Settings::default() + .parse(&["date", "-R", "-R", "--help"]) + .unwrap_err(); + assert_eq!(the_err.exit_code, 1); + match the_err.kind { + uutils_args::ErrorKind::UnexpectedArgument(arg) => { + assert_eq!(arg, MAGIC_MULTI_OUTPUT_ARG); + } + _ => panic!("wrong error kind: {:?}", the_err.kind), + } +} + +/// https://github.com/uutils/coreutils/issues/4254#issuecomment-2026446634 +#[test] +fn priority_demo() { + // Earliest faulty argument is the first argument, must complaint about that: + let the_err = Settings::default() + .parse(&["date", "-Idefinitely_invalid", "-R", "-R"]) + .unwrap_err(); + match the_err.kind { + uutils_args::ErrorKind::ParsingFailed { option, value, .. } => { + assert_eq!(option, "-I"); + assert_eq!(value, "definitely_invalid"); + } + _ => panic!("wrong error kind: {:?}", the_err.kind), + } + // Earliest faulty argument is the second argument, must complaint about that: + let the_err = Settings::default() + .parse(&["date", "-R", "-R", "-Idefinitely_invalid"]) + .unwrap_err(); + match the_err.kind { + uutils_args::ErrorKind::UnexpectedArgument(arg) => { + assert_eq!(arg, MAGIC_MULTI_OUTPUT_ARG); + } + _ => panic!("wrong error kind: {:?}", the_err.kind), + } + // Earliest faulty argument is the second argument, must complaint about that: + let the_err = Settings::default() + .parse(&["date", "-R", "-Idefinitely_invalid", "-R"]) + .unwrap_err(); + match the_err.kind { + uutils_args::ErrorKind::ParsingFailed { option, value, .. } => { + assert_eq!(option, "-I"); + assert_eq!(value, "definitely_invalid"); + } + _ => panic!("wrong error kind: {:?}", the_err.kind), + } + // Earliest faulty argument is the second argument, must complaint about that: + let the_err = Settings::default() + .parse(&["date", "-R", "-Ins", "-R"]) + .unwrap_err(); + match the_err.kind { + uutils_args::ErrorKind::UnexpectedArgument(arg) => { + assert_eq!(arg, MAGIC_MULTI_OUTPUT_ARG); + } + _ => panic!("wrong error kind: {:?}", the_err.kind), + } +} From e3f0b529fab34aca0013f12938efa12498da4a6b Mon Sep 17 00:00:00 2001 From: Ben Wiederhake Date: Fri, 29 Mar 2024 14:05:32 +0100 Subject: [PATCH 103/116] Mark unused spaces to satisfy clippy This causes CI failures, so let's fix this. --- examples/completion.rs | 4 ++-- tests/coreutils/dd.rs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/completion.rs b/examples/completion.rs index bcfda9a..f9e4cce 100644 --- a/examples/completion.rs +++ b/examples/completion.rs @@ -22,12 +22,12 @@ enum Arg { // Completion is derived from the `Number` type, through the `Value` trait /// Give it a number! #[arg("-n N", "--number=N")] - Number(Number), + Number(#[allow(unused)] Number), // Completion is derived from the `PathBuf` type /// Give it a path! #[arg("-p P", "--path=P")] - Path(PathBuf), + Path(#[allow(unused)] PathBuf), } struct Settings; diff --git a/tests/coreutils/dd.rs b/tests/coreutils/dd.rs index b8b5358..32cbe1a 100644 --- a/tests/coreutils/dd.rs +++ b/tests/coreutils/dd.rs @@ -32,7 +32,7 @@ enum Arg { Bs(usize), #[arg("cbs=BYTES")] - Cbs(usize), + Cbs(#[allow(unused)] usize), #[arg("skip=BYTES", "iseek=BYTES")] Skip(u64), @@ -47,13 +47,13 @@ enum Arg { Status(StatusLevel), #[arg("conv=CONVERSIONS")] - Conv(String), + Conv(#[allow(unused)] String), #[arg("iflag=FLAGS")] - Iflag(String), + Iflag(#[allow(unused)] String), #[arg("oflag=FLAGS")] - Oflag(String), + Oflag(#[allow(unused)] String), } #[derive(Debug, PartialEq, Eq)] From 6d2b4f423b689214c91610d45053bed939a56804 Mon Sep 17 00:00:00 2001 From: Ben Wiederhake Date: Sat, 30 Mar 2024 21:07:11 +0100 Subject: [PATCH 104/116] fix documented list of supported shell completions --- complete/src/lib.rs | 2 +- docs/guide/completions.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/complete/src/lib.rs b/complete/src/lib.rs index 5d8649c..ee84b8e 100644 --- a/complete/src/lib.rs +++ b/complete/src/lib.rs @@ -76,6 +76,6 @@ pub fn render(c: &Command, shell: &str) -> String { "nu" | "nushell" => nu::render(c), "man" => man::render(c), "sh" | "bash" | "csh" | "elvish" | "powershell" => panic!("shell '{shell}' completion is not implemented yet!"), - _ => panic!("unknown option '{shell}'! Expected one of: \"md\", \"fish\", \"zsh\", \"man\", \"sh\", \"bash\", \"csh\", \"elvish\", \"powershell\""), + _ => panic!("unknown option '{shell}'! Expected one of: \"md\", \"fish\", \"zsh\", \"nu[shell]\", \"man\", \"sh\", \"bash\", \"csh\", \"elvish\", \"powershell\""), } } diff --git a/docs/guide/completions.md b/docs/guide/completions.md index f325b91..d89d3dd 100644 --- a/docs/guide/completions.md +++ b/docs/guide/completions.md @@ -33,7 +33,7 @@ Shell completions and documentation can be generated automatically by this crate cargo run --features parse-is-complete -- [shell] ``` -The `[shell]` value here can be `fish`, `zsh`, `bash`, `powershell`, `elvish` or `nu`. +The `[shell]` value here can be `fish`, `zsh`, `nu`, `sh`, `bash`, `csh`, `elvish`, or `powershell`. > **Note**: Some of these remain unimplemented as of writing. @@ -47,4 +47,4 @@ If you do not want to hijack the [`Options::parse`](crate::Options::parse) funct [Up](super) [Next]() - \ No newline at end of file + From 1b5405cb0d8a888c0f892f91f9c9965c11ebb47e Mon Sep 17 00:00:00 2001 From: Ben Wiederhake Date: Sat, 30 Mar 2024 22:37:04 +0100 Subject: [PATCH 105/116] implement rudimentary bash completion --- complete/src/bash.rs | 64 ++++++++++++++++++++++++++++++++++++++++++++ complete/src/lib.rs | 4 ++- 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 complete/src/bash.rs diff --git a/complete/src/bash.rs b/complete/src/bash.rs new file mode 100644 index 0000000..37b7711 --- /dev/null +++ b/complete/src/bash.rs @@ -0,0 +1,64 @@ +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use crate::{Command, Flag}; + +/// Create completion script for `bash` +/// +/// Short and long options are combined into single `complete` calls, even if +/// they differ in whether they take arguments or not; just like in case of `fish`. +/// Also, pretend that files are fine in any position. ValueHints are ignored entirely. +pub fn render(c: &Command) -> String { + let mut out = String::new(); + let name = &c.name; + // Register _comp_uu_FOO as a bash function that computes completions: + out.push_str(&format!("complete -F _comp_uu_{name} {name};")); + out.push_str(&format!("_comp_uu_{name}()")); + // Unless the current argument starts with "-", pre-populate the completions list with all files and dirs: + out.push_str("{ local cur;_init_completion||return;COMPREPLY=();if [[ \"$cur\" != \"-*\" ]]; then _filedir;fi;COMPREPLY+=($(compgen -W \""); + for arg in &c.args { + for Flag { flag, .. } in &arg.short { + out.push_str(&format!("-{flag} ")); + } + for Flag { flag, .. } in &arg.long { + out.push_str(&format!("--{flag} ")); + } + } + out.push_str("\" -- \"$cur\"));}\n"); + out +} + +#[cfg(test)] +mod test { + use super::render; + use crate::{Arg, Command, Flag, Value}; + + #[test] + fn simple() { + let c = Command { + name: "foo", + args: vec![ + Arg { + short: vec![Flag { + flag: "a", + value: Value::No, + }], + long: vec![Flag { + flag: "all", + value: Value::No, + }], + ..Arg::default() + }, + Arg { + short: vec![Flag { + flag: "x", + value: Value::No, + }], + ..Arg::default() + }, + ], + ..Command::default() + }; + assert_eq!(render(&c), "complete -F _comp_uu_foo foo;_comp_uu_foo(){ local cur;_init_completion||return;COMPREPLY=();if [[ \"$cur\" != \"-*\" ]]; then _filedir;fi;COMPREPLY+=($(compgen -W \"-a --all -x \" -- \"$cur\"));}\n") + } +} diff --git a/complete/src/lib.rs b/complete/src/lib.rs index ee84b8e..39cbd62 100644 --- a/complete/src/lib.rs +++ b/complete/src/lib.rs @@ -13,6 +13,7 @@ //! - Some information is removed because it is irrelevant for completion and documentation //! - This struct is meant to exist at runtime of the program //! +mod bash; mod fish; mod man; mod md; @@ -75,7 +76,8 @@ pub fn render(c: &Command, shell: &str) -> String { "zsh" => zsh::render(c), "nu" | "nushell" => nu::render(c), "man" => man::render(c), - "sh" | "bash" | "csh" | "elvish" | "powershell" => panic!("shell '{shell}' completion is not implemented yet!"), + "bash" => bash::render(c), + "sh" | "csh" | "elvish" | "powershell" => panic!("shell '{shell}' completion is not implemented yet!"), _ => panic!("unknown option '{shell}'! Expected one of: \"md\", \"fish\", \"zsh\", \"nu[shell]\", \"man\", \"sh\", \"bash\", \"csh\", \"elvish\", \"powershell\""), } } From e752a3f7835afd42c30fe4b612a31a067d7b8322 Mon Sep 17 00:00:00 2001 From: Ben Wiederhake Date: Sun, 31 Mar 2024 15:21:19 +0200 Subject: [PATCH 106/116] handle special-case program name '[' --- complete/src/bash.rs | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/complete/src/bash.rs b/complete/src/bash.rs index 37b7711..265c2c0 100644 --- a/complete/src/bash.rs +++ b/complete/src/bash.rs @@ -10,10 +10,14 @@ use crate::{Command, Flag}; /// Also, pretend that files are fine in any position. ValueHints are ignored entirely. pub fn render(c: &Command) -> String { let mut out = String::new(); - let name = &c.name; + // Be careful around the program '['! + let name_identifier = if c.name == "[" { &"bracket" } else { &c.name }; // Register _comp_uu_FOO as a bash function that computes completions: - out.push_str(&format!("complete -F _comp_uu_{name} {name};")); - out.push_str(&format!("_comp_uu_{name}()")); + out.push_str(&format!( + "complete -F _comp_uu_{name_identifier} '{}';", + &c.name + )); + out.push_str(&format!("_comp_uu_{name_identifier}()")); // Unless the current argument starts with "-", pre-populate the completions list with all files and dirs: out.push_str("{ local cur;_init_completion||return;COMPREPLY=();if [[ \"$cur\" != \"-*\" ]]; then _filedir;fi;COMPREPLY+=($(compgen -W \""); for arg in &c.args { @@ -59,6 +63,22 @@ mod test { ], ..Command::default() }; - assert_eq!(render(&c), "complete -F _comp_uu_foo foo;_comp_uu_foo(){ local cur;_init_completion||return;COMPREPLY=();if [[ \"$cur\" != \"-*\" ]]; then _filedir;fi;COMPREPLY+=($(compgen -W \"-a --all -x \" -- \"$cur\"));}\n") + assert_eq!(render(&c), "complete -F _comp_uu_foo 'foo';_comp_uu_foo(){ local cur;_init_completion||return;COMPREPLY=();if [[ \"$cur\" != \"-*\" ]]; then _filedir;fi;COMPREPLY+=($(compgen -W \"-a --all -x \" -- \"$cur\"));}\n") + } + + #[test] + fn bracket() { + let c = Command { + name: "[", + args: vec![Arg { + short: vec![Flag { + flag: "x", + value: Value::No, + }], + ..Arg::default() + }], + ..Command::default() + }; + assert_eq!(render(&c), "complete -F _comp_uu_bracket '[';_comp_uu_bracket(){ local cur;_init_completion||return;COMPREPLY=();if [[ \"$cur\" != \"-*\" ]]; then _filedir;fi;COMPREPLY+=($(compgen -W \"-x \" -- \"$cur\"));}\n") } } From f1eb49937ec0370737c5fdff0fd17df1633f05e5 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Tue, 1 Apr 2025 11:17:55 +0200 Subject: [PATCH 107/116] Remove ref --- derive/src/argument.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/src/argument.rs b/derive/src/argument.rs index dec13c2..94b84eb 100644 --- a/derive/src/argument.rs +++ b/derive/src/argument.rs @@ -187,7 +187,7 @@ pub fn long_handling(args: &[Argument], help_flags: &Flags) -> TokenStream { ArgType::Option { flags, takes_value, - ref default, + default, hidden: _, } => (flags, takes_value, default), ArgType::Free { .. } => continue, From 6b2f3d5aa58c1c467f4bd692c86954ac8efad2ec Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Tue, 1 Apr 2025 11:19:11 +0200 Subject: [PATCH 108/116] Switch to rust edition 2024 --- Cargo.toml | 2 +- complete/Cargo.toml | 4 ++-- derive/Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 660520f..e9c6517 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "uutils-args" version = "0.1.0" -edition = "2021" +edition = "2024" authors = ["Terts Diepraam"] license = "MIT" diff --git a/complete/Cargo.toml b/complete/Cargo.toml index 16c73d0..c9bae20 100644 --- a/complete/Cargo.toml +++ b/complete/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "uutils-args-complete" version = "0.1.0" -edition = "2021" +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" \ No newline at end of file +roff = "0.2.1" diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 1e6b1a3..0a2f39c 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "uutils-args-derive" version = "0.1.0" -edition = "2021" +edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 7a126b5f457a627698ce05e7302899b42f7787c9 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Tue, 1 Apr 2025 11:19:52 +0200 Subject: [PATCH 109/116] Adapt formatting --- complete/src/bash.rs | 10 ++++++-- complete/src/lib.rs | 8 +++++-- complete/src/man.rs | 2 +- derive/src/attributes.rs | 46 +++++++++++++++++++------------------ derive/src/flags.rs | 24 +++++++++++-------- derive/src/lib.rs | 2 +- tests/coreutils/base32.rs | 2 +- tests/coreutils/basename.rs | 2 +- tests/coreutils/mktemp.rs | 2 +- tests/coreutils/shuf.rs | 2 +- 10 files changed, 59 insertions(+), 41 deletions(-) diff --git a/complete/src/bash.rs b/complete/src/bash.rs index 265c2c0..e72afad 100644 --- a/complete/src/bash.rs +++ b/complete/src/bash.rs @@ -63,7 +63,10 @@ mod test { ], ..Command::default() }; - assert_eq!(render(&c), "complete -F _comp_uu_foo 'foo';_comp_uu_foo(){ local cur;_init_completion||return;COMPREPLY=();if [[ \"$cur\" != \"-*\" ]]; then _filedir;fi;COMPREPLY+=($(compgen -W \"-a --all -x \" -- \"$cur\"));}\n") + assert_eq!( + render(&c), + "complete -F _comp_uu_foo 'foo';_comp_uu_foo(){ local cur;_init_completion||return;COMPREPLY=();if [[ \"$cur\" != \"-*\" ]]; then _filedir;fi;COMPREPLY+=($(compgen -W \"-a --all -x \" -- \"$cur\"));}\n" + ) } #[test] @@ -79,6 +82,9 @@ mod test { }], ..Command::default() }; - assert_eq!(render(&c), "complete -F _comp_uu_bracket '[';_comp_uu_bracket(){ local cur;_init_completion||return;COMPREPLY=();if [[ \"$cur\" != \"-*\" ]]; then _filedir;fi;COMPREPLY+=($(compgen -W \"-x \" -- \"$cur\"));}\n") + assert_eq!( + render(&c), + "complete -F _comp_uu_bracket '[';_comp_uu_bracket(){ local cur;_init_completion||return;COMPREPLY=();if [[ \"$cur\" != \"-*\" ]]; then _filedir;fi;COMPREPLY+=($(compgen -W \"-x \" -- \"$cur\"));}\n" + ) } } diff --git a/complete/src/lib.rs b/complete/src/lib.rs index 39cbd62..cfad348 100644 --- a/complete/src/lib.rs +++ b/complete/src/lib.rs @@ -77,7 +77,11 @@ pub fn render(c: &Command, shell: &str) -> String { "nu" | "nushell" => nu::render(c), "man" => man::render(c), "bash" => bash::render(c), - "sh" | "csh" | "elvish" | "powershell" => panic!("shell '{shell}' completion is not implemented yet!"), - _ => panic!("unknown option '{shell}'! Expected one of: \"md\", \"fish\", \"zsh\", \"nu[shell]\", \"man\", \"sh\", \"bash\", \"csh\", \"elvish\", \"powershell\""), + "sh" | "csh" | "elvish" | "powershell" => { + panic!("shell '{shell}' completion is not implemented yet!") + } + _ => panic!( + "unknown option '{shell}'! Expected one of: \"md\", \"fish\", \"zsh\", \"nu[shell]\", \"man\", \"sh\", \"bash\", \"csh\", \"elvish\", \"powershell\"" + ), } } diff --git a/complete/src/man.rs b/complete/src/man.rs index 046cf08..0e0a38c 100644 --- a/complete/src/man.rs +++ b/complete/src/man.rs @@ -2,7 +2,7 @@ // file that was distributed with this source code. use crate::{Command, Flag, Value}; -use roff::{bold, italic, roman, Roff}; +use roff::{Roff, bold, italic, roman}; pub fn render(c: &Command) -> String { let mut page = Roff::new(); diff --git a/derive/src/attributes.rs b/derive/src/attributes.rs index a517f14..da582f8 100644 --- a/derive/src/attributes.rs +++ b/derive/src/attributes.rs @@ -2,7 +2,7 @@ // file that was distributed with this source code. use syn::{ - meta::ParseNestedMeta, parse::ParseStream, Attribute, Expr, Ident, LitInt, LitStr, Token, + Attribute, Expr, Ident, LitInt, LitStr, Token, meta::ParseNestedMeta, parse::ParseStream, }; use crate::flags::Flags; @@ -141,7 +141,7 @@ impl OptionAttr { return Err(syn::Error::new_spanned( ident, "unrecognized argument for option attribute", - )) + )); } } Ok(()) @@ -186,27 +186,29 @@ impl ValueAttr { return Ok(value_attr); } - attr.parse_args_with(|s: ParseStream| loop { - if let Ok(litstr) = s.parse::() { - value_attr.keys.push(litstr.value()); - } else { - let ident = s.parse::()?; - match ident.to_string().as_str() { - "value" => { - s.parse::()?; - let p = s.parse::()?; - value_attr.value = Some(p); + attr.parse_args_with(|s: ParseStream| { + loop { + if let Ok(litstr) = s.parse::() { + value_attr.keys.push(litstr.value()); + } else { + let ident = s.parse::()?; + match ident.to_string().as_str() { + "value" => { + s.parse::()?; + let p = s.parse::()?; + value_attr.value = Some(p); + } + _ => return Err(s.error("unrecognized keyword in value attribute")), } - _ => return Err(s.error("unrecognized keyword in value attribute")), } - } - if s.is_empty() { - return Ok(()); - } - s.parse::()?; - if s.is_empty() { - return Ok(()); + if s.is_empty() { + return Ok(()); + } + s.parse::()?; + if s.is_empty() { + return Ok(()); + } } })?; @@ -244,7 +246,7 @@ fn assert_expr_is_array_of_litstr(expr: Expr, flag: &str) -> syn::Result syn::Result Date: Tue, 1 Apr 2025 17:59:03 +0200 Subject: [PATCH 110/116] adjust the README --- README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 15801e5..66ea259 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,16 @@ +[![Discord](https://img.shields.io/badge/discord-join-7289DA.svg?logo=discord&longCache=true&style=flat)](https://discord.gg/wQVJbvJ) +[![License](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/uutils/uutils-args/blob/main/LICENSE) +[![dependency status](https://deps.rs/repo/github/uutils/uutils-args/status.svg)](https://deps.rs/repo/github/uutils/uutils-args) + +[![CodeCov](https://codecov.io/gh/uutils/uutils-args/branch/master/graph/badge.svg)](https://codecov.io/gh/uutils/uutils-args) +![MSRV](https://img.shields.io/badge/MSRV-1.85.0-brightgreen) + # uutils-args -Argument parsing for the [uutils coreutils](https://www.github.com/uutils/coreutils) project. +Argument parsing for the [uutils](https://www.github.com/uutils/) projects. It is designed to be flexible, while providing default -behaviour that aligns with GNU coreutils. +behaviour that aligns with GNU coreutils and other tools. ## Features From 522f1850e2acc766f429fc2a087ee6624586b634 Mon Sep 17 00:00:00 2001 From: Ben Wiederhake Date: Sat, 30 Mar 2024 19:41:26 +0100 Subject: [PATCH 111/116] 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 112/116] 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 113/116] 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 114/116] 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 115/116] 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 116/116] 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),