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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da3847e..ee5dafd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,75 +16,50 @@ 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: - toolchain: stable - profile: minimal - override: true + uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - - uses: actions-rs/cargo@v1 - with: - command: test - args: --all-features --workspace + - run: cargo test --workspace rustfmt: name: Rustfmt 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 + uses: dtolnay/rust-toolchain@stable with: - toolchain: stable - profile: minimal - override: true 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 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 + uses: dtolnay/rust-toolchain@stable with: - toolchain: stable - profile: minimal - override: true components: clippy - uses: Swatinem/rust-cache@v2 - name: Clippy check - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --all-targets --all-features --workspace -- -D warnings + run: cargo clippy --all-targets --workspace -- -D warnings docs: name: Docs 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: - toolchain: stable - profile: minimal - override: true + uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - name: Check documentation env: RUSTDOCFLAGS: -D warnings - uses: actions-rs/cargo@v1 - with: - command: doc + run: cargo doc 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/Cargo.toml b/Cargo.toml index 4a5b26f..89aedf9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,25 @@ [package] name = "uutils-args" version = "0.1.0" -edition = "2021" +edition = "2024" 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] -derive = { version = "0.1.0", path = "derive" } lexopt = "0.3.0" +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 = [] [workspace] -members = [ - "derive", -] +members = ["derive"] diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..c7cea4d --- /dev/null +++ b/Justfile @@ -0,0 +1,5 @@ +check: + cargo fmt --all + cargo test + cargo clippy --all-targets --workspace -- -D warnings + cargo doc 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/README.md b/README.md index 2f71c24..66ea259 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,129 @@ +[![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 -An experimental derive-based argument parser specifically for uutils +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 and other tools. + +## 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 (for example: [clap](https://github.com/clap-rs/clap)). + +## 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, Options}; + +#[derive(Arguments)] +enum Arg { + // The doc strings below will be part of the `--help` text + // First we define a simple flag: + /// Transform input text to uppercase + #[arg("-c", "--caps")] + Caps, + + // This option takes a value: + /// Add exclamation marks to output + #[arg("-e N", "--exclaim=N")] + ExclamationMarks(u8), +} + +#[derive(Default)] +struct Settings { + 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) -> Result<(), uutils_args::Error> { + match arg { + Arg::Caps => self.caps = true, + Arg::ExclamationMarks(n) => self.exclamation_marks += n, + } + Ok(()) + } +} + +fn run(args: &[&str]) -> String { + 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() + } else { + 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", "--caps", "hello"]), "HELLO"); +assert_eq!(run(&["shout", "-e3", "-c", "hello"]), "HELLO!!!"); +assert_eq!(run(&["shout", "-e3", "-c", "hello", "world"]), "HELLO WORLD!!!"); +``` + +## Value parsing + +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 + +The following files contain examples of commands defined with +`uutils_args`: + +- [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/derive/Cargo.toml b/derive/Cargo.toml index 2b73b0d..0a2f39c 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -1,15 +1,14 @@ [package] -name = "derive" +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 [lib] -proc_macro = true +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"] } +proc-macro2 = "1.0.81" +quote = "1.0.36" +syn = { version = "2.0.60", features = ["full"] } 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 46c3289..7a09b2b 100644 --- a/derive/src/argument.rs +++ b/derive/src/argument.rs @@ -1,38 +1,35 @@ -use std::ops::RangeInclusive; +// 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; 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 field: Option, + pub arg_type: ArgType, + pub help: String, } -pub(crate) enum ArgType { +pub enum ArgType { Option { flags: Flags, hidden: bool, takes_value: bool, default: TokenStream, }, - Positional { - num_args: RangeInclusive, - last: bool, - }, Free { filters: Vec, }, } -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(); @@ -41,10 +38,10 @@ 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(); + 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() { @@ -75,7 +72,7 @@ pub(crate) 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()), }; @@ -89,20 +86,13 @@ pub(crate) 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, }, }; Argument { ident: ident.clone(), - name: name.clone(), + field: field.clone(), arg_type, help: arg_help, } @@ -135,16 +125,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(); @@ -156,7 +142,6 @@ pub(crate) fn short_handling(args: &[Argument]) -> (TokenStream, Vec) { ref default, hidden: _, } => (flags, takes_value, default), - ArgType::Positional { .. } => continue, ArgType::Free { .. } => continue, }; @@ -185,14 +170,14 @@ pub(crate) fn short_handling(args: &[Argument]) -> (TokenStream, Vec) { Ok(Some(Argument::Custom( match short { #(#match_arms)* - _ => return Err(Error::UnexpectedOption(short.to_string())), + _ => return Err(::uutils_args::ErrorKind::UnexpectedOption(short.to_string(), Vec::new())), } ))) ); (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(); @@ -203,10 +188,9 @@ pub(crate) fn long_handling(args: &[Argument], help_flags: &Flags) -> TokenStrea ArgType::Option { flags, takes_value, - ref default, + default, hidden: _, } => (flags, takes_value, default), - ArgType::Positional { .. } => continue, ArgType::Free { .. } => continue, }; @@ -231,14 +215,19 @@ pub(crate) fn long_handling(args: &[Argument], help_flags: &Flags) -> TokenStrea } if options.is_empty() { - return quote!(return Err(Error::UnexpectedOption(long.to_string()))); + return quote!( + return Err(::uutils_args::ErrorKind::UnexpectedOption( + long.to_string(), + Vec::new() + )) + ); } // TODO: Add version check let help_check = if !help_flags.long.is_empty() { let long_help_flags = help_flags.long.iter().map(|f| &f.flag); quote!(if let #(#long_help_flags)|* = long { - return Ok(Some(Argument::Help)); + return Ok(Some(::uutils_args::Argument::Help)); }) } else { quote!() @@ -248,26 +237,7 @@ pub(crate) fn long_handling(args: &[Argument], help_flags: &Flags) -> TokenStrea quote!( let long_options: [&str; #num_opts] = [#(#options),*]; - let mut candidates = Vec::new(); - let mut exact_match = None; - for opt in long_options { - if opt == long { - exact_match = Some(opt); - break; - } else if opt.starts_with(long) { - candidates.push(opt); - } - } - - let long = match (exact_match, &candidates[..]) { - (Some(opt), _) => opt, - (None, [opt]) => opt, - (None, []) => return Err(Error::UnexpectedOption(long.to_string())), - (None, opts) => return Err(Error::AmbiguousOption { - option: long.to_string(), - candidates: candidates.iter().map(|s| s.to_string()).collect(), - }) - }; + let long = ::uutils_args::internal::infer_long_option(long, &long_options)?; #help_check @@ -281,14 +251,13 @@ 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 for arg @ Argument { arg_type, .. } in args { let filters = match arg_type { ArgType::Free { filters } => filters, - ArgType::Positional { .. } => continue, ArgType::Option { .. } => continue, }; @@ -297,7 +266,7 @@ pub(crate) 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)))); } @@ -307,19 +276,20 @@ pub(crate) fn free_handling(args: &[Argument]) -> TokenStream { // dd-style arguments let mut dd_branches = Vec::new(); + let mut dd_args = Vec::new(); for arg @ Argument { arg_type, .. } in args { let flags = match arg_type { ArgType::Option { flags, .. } => flags, ArgType::Free { .. } => continue, - ArgType::Positional { .. } => continue, }; for (prefix, _) in &flags.dd_style { let ident = &arg.ident; + 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)))); } @@ -331,80 +301,22 @@ pub(crate) fn free_handling(args: &[Argument]) -> TokenStream { if_expressions.push(quote!( if let Some((prefix, value)) = arg.split_once('=') { #(#dd_branches)* + + return Err(::uutils_args::ErrorKind::UnexpectedOption( + prefix.to_string(), + ::uutils_args::internal::filter_suggestions(prefix, &[#(#dd_args),*], "") + )); } )); } - quote!(if let Some(mut raw) = parser.try_raw_args() { - if let Some(arg) = raw.peek().and_then(|s| s.to_str()) { - #(#if_expressions)* - } - }) -} - -pub(crate) 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()), + quote!( + if let Some(mut raw) = parser.try_raw_args() { + if let Some(arg) = raw.peek().and_then(|s| s.to_str()) { + #(#if_expressions)* } - ))) - ); - - 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::MissingPositionalArguments( - missing.iter().map(ToString::to_string).collect::>() - )) - } else { - Ok(()) - } - ); - - (value_handling, missing_argument_checks) + ) } fn no_value_expression(ident: &Ident) -> TokenStream { @@ -417,30 +329,11 @@ 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()?)?)) -} - -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)?) - ) -} - -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::parse_value_for_option("", &v)) - .collect::>()?; - Self::#ident(collection) - }) + quote!(Self::#ident(::uutils_args::internal::parse_value_for_option(&option, &parser.value()?)?)) } diff --git a/derive/src/attributes.rs b/derive/src/attributes.rs index 2a19604..8604b1e 100644 --- a/derive/src/attributes.rs +++ b/derive/src/attributes.rs @@ -1,89 +1,19 @@ -use std::ops::RangeInclusive; +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. use syn::{ - meta::ParseNestedMeta, parse::ParseStream, Attribute, Expr, ExprLit, ExprRange, Ident, Lit, - LitInt, LitStr, RangeLimits, Token, + Attribute, Expr, Ident, LitInt, LitStr, Token, meta::ParseNestedMeta, parse::ParseStream, }; 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, + pub options_first: bool, } impl Default for ArgumentsAttr { @@ -94,12 +24,13 @@ impl Default for ArgumentsAttr { file: None, exit_code: 1, parse_echo_style: false, + options_first: false, } } } 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| { @@ -126,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(()) @@ -135,36 +69,66 @@ impl ArgumentsAttr { } } +#[allow(clippy::large_enum_variant)] +pub enum ArgAttr { + Option(OptionAttr), + 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 { + panic!("Could not determine type of argument"); + } + } 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 value: 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::()?; 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; @@ -178,7 +142,7 @@ impl OptionAttr { return Err(syn::Error::new_spanned( ident, "unrecognized argument for option attribute", - )) + )); } } Ok(()) @@ -189,15 +153,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(()) @@ -208,13 +173,13 @@ impl FreeAttr { } #[derive(Default)] -pub(crate) struct ValueAttr { - pub(crate) keys: Vec, - pub(crate) value: Option, +pub struct ValueAttr { + pub keys: Vec, + pub value: Option, } impl ValueAttr { - pub(crate) fn parse(attr: &Attribute) -> syn::Result { + 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 @@ -222,100 +187,85 @@ impl ValueAttr { return Ok(value_attr); } - parse_args(attr, |s: ParseStream| { - if let Ok(litstr) = s.parse::() { - value_attr.keys.push(litstr.value()); - return Ok(()); - } + 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")), + } + } - let ident = s.parse::()?; - match ident.to_string().as_str() { - "value" => { - s.parse::()?; - let p = s.parse::()?; - value_attr.value = Some(p); + if s.is_empty() { + return Ok(()); + } + s.parse::()?; + if s.is_empty() { + return Ok(()); } - _ => 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, -} - -impl Default for PositionalAttr { - fn default() -> Self { - Self { - num_args: 1..=1, - last: false, +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)?; } } -impl PositionalAttr { - pub(crate) fn parse(attr: &Attribute) -> syn::Result { - let mut positional_attr = Self::default(); - parse_args(attr, |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(()); - } +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")), + } +} - 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(()); - } +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 ident = s.parse::()?; - match ident.to_string().as_str() { - "last" => positional_attr.last = true, - _ => return Err(s.error("unrecognized keyword in value attribute")), + 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"), + )); } - Ok(()) - })?; - - Ok(positional_attr) + }; + strings.push(val); } + Ok(strings) } diff --git a/derive/src/complete.rs b/derive/src/complete.rs new file mode 100644 index 0000000..9e919d1 --- /dev/null +++ b/derive/src/complete.rs @@ -0,0 +1,110 @@ +// 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, Value}, +}; +use proc_macro2::TokenStream; +use quote::quote; + +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, + 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; + } + + // 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 }| { + 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)) + } + }; + quote!(::uutils_args::complete::Flag { + flag: #flag, + value: #value + }) + }) + .collect(); + + let long: Vec<_> = long + .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)) + } + }; + quote!(::uutils_args::complete::Flag { + flag: #flag, + value: #value + }) + }) + .collect(); + + let hint = match (field, any_flag_takes_argument) { + (Some(ty), true) => quote!(Some(<#ty>::value_hint())), + _ => quote!(None), + }; + + arg_specs.push(quote!( + ::uutils_args::complete::Arg { + short: vec![#(#short),*], + long: vec![#(#long),*], + help: #help, + value: #hint, + } + )) + } + + 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),*], + license: env!("CARGO_PKG_LICENSE"), + authors: env!("CARGO_PKG_AUTHORS"), + }) +} diff --git a/derive/src/flags.rs b/derive/src/flags.rs index b999f0a..da7497e 100644 --- a/derive/src/flags.rs +++ b/derive/src/flags.rs @@ -1,28 +1,31 @@ +// 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; #[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 { +#[derive(Clone, PartialEq, Eq)] +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()); @@ -30,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 @@ -57,11 +60,14 @@ impl Flags { } else if sep == '[' { let optional = val .strip_prefix('=') - .and_then(|s| s.strip_suffix(']')) - .unwrap(); - assert!(optional - .chars() - .all(|c: char| c.is_alphanumeric() || c == '-')); + .expect("expected '=' after '[' in flag pattern") + .strip_suffix(']') + .expect("expected final ']' in flag pattern"); + assert!( + optional + .chars() + .all(|c: char| c.is_alphanumeric() || c == '-') + ); Value::Optional(optional.into()) } else { panic!("Invalid long flag '{flag}'"); @@ -69,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 @@ -78,21 +82,27 @@ 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: let value = if val.is_empty() { Value::No } else if let Some(optional) = val.strip_prefix('[').and_then(|s| s.strip_suffix(']')) { - assert!(optional - .chars() - .all(|c: char| c.is_alphanumeric() || c == '-')); + assert!( + optional + .chars() + .all(|c: char| c.is_alphanumeric() || c == '-') + ); Value::Optional(optional.into()) } else if let Some(required) = val.strip_prefix(' ') { - assert!(required - .chars() - .all(|c: char| c.is_alphanumeric() || c == '-')); + assert!( + required + .chars() + .all(|c: char| c.is_alphanumeric() || c == '-') + ); Value::Required(required.into()) } else { panic!("Invalid short flag '{flag}'") @@ -107,11 +117,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[..]) { @@ -124,7 +134,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 a853b77..2c99c3a 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}, @@ -11,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!(); } @@ -25,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, @@ -48,7 +51,6 @@ pub(crate) 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 { .. } => {} } @@ -72,67 +74,40 @@ pub(crate) 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::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 ) } -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 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), @@ -141,7 +116,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/help_parser.rs b/derive/src/help_parser.rs index 8faa4e6..16a0dba 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. @@ -73,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(); diff --git a/derive/src/initial.rs b/derive/src/initial.rs deleted file mode 100644 index 1cb0e83..0000000 --- a/derive/src/initial.rs +++ /dev/null @@ -1,133 +0,0 @@ -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 e78c358..73a08a2 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -1,27 +1,28 @@ +// 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; mod flags; mod help; mod help_parser; -mod initial; 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}; 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) -} +use syn::{Data::Enum, DeriveInput, parse_macro_input}; -#[proc_macro_derive(Arguments, attributes(flag, option, positional, free, arguments))] +/// 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); @@ -38,15 +39,14 @@ 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, &arguments_attr.version_flags, &arguments_attr.file, ); + 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!( @@ -58,7 +58,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()? @@ -67,17 +67,30 @@ 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; #[allow(unreachable_code)] fn next_arg( - parser: &mut uutils_args::lexopt::Parser, positional_idx: &mut usize - ) -> Result>, uutils_args::Error> { - use uutils_args::{Value, lexopt, Error, Argument}; + parser: &mut ::uutils_args::lexopt::Parser + ) -> Result>, ::uutils_args::ErrorKind> { + use ::uutils_args::{Value, lexopt, Error, Argument}; - // #number_argment #free let arg = match { #next_arg } { @@ -96,23 +109,25 @@ pub fn arguments(input: TokenStream) -> TokenStream { } } - fn check_missing(positional_idx: usize) -> Result<(), uutils_args::Error> { - #missing_argument_checks - } - - fn help(bin_name: &str) -> ::std::io::Result<()> { + fn help(bin_name: &str) -> String { #help_string } fn version() -> String { #version_string } + + fn complete() -> ::uutils_args::complete::Command<'static> { + use ::uutils_args::Value; + #complete_command + } } ); 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); @@ -127,6 +142,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(); @@ -135,7 +151,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()] @@ -143,19 +160,22 @@ pub fn value(input: TokenStream) -> TokenStream { keys }; + all_keys.extend(keys.clone()); 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 { @@ -187,10 +207,20 @@ pub fn value(input: TokenStream) -> TokenStream { }; Ok(match opt { - #(#match_arms),*, + #(#match_arms)* _ => unreachable!("Should be caught by (None, []) case above.") }) } + + fn value_hint() -> ::uutils_args::complete::ValueHint { + let keys: [&str; #keys_len] = [#(#all_keys),*]; + ::uutils_args::complete::ValueHint::Strings( + keys + .into_iter() + .map(ToString::to_string) + .collect() + ) + } } ); diff --git a/design/design.md b/design/design.md deleted file mode 100644 index 9754b42..0000000 --- a/design/design.md +++ /dev/null @@ -1,213 +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, -} -``` \ No newline at end of file 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/design/README.md b/docs/design/design.md similarity index 59% rename from design/README.md rename to docs/design/design.md index 164bca4..c79b192 100644 --- a/design/README.md +++ b/docs/design/design.md @@ -1,7 +1,8 @@ -# `uutils-args` Design Docs +# Design -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. +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. @@ -14,10 +15,10 @@ 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 +## Chapters -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) +1. [Arguments in the coreutils](design::coreutils) +2. [Problems with `clap`](design::problems) diff --git a/design/problems_with_clap.md b/docs/design/problems_with_clap.md similarity index 89% rename from design/problems_with_clap.md rename to docs/design/problems_with_clap.md index 9293984..308b2ac 100644 --- a/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 @@ -107,11 +108,11 @@ 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: -```rust +```rust,ignore const OPT_NAME: &'static str = "name"; // -- snip -- @@ -134,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 @@ -149,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 @@ -183,9 +194,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/tertsdiepraam/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..e7d04c1 --- /dev/null +++ b/docs/guide/completions.md @@ -0,0 +1,50 @@ + +
+ +[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. + +```bash +cargo run --features parse-is-complete -- [shell] +``` + +The `[shell]` value here can be `fish`, `zsh`, `nu`, `sh`, `bash`, `csh`, `elvish`, or `powershell`. + +> **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 use 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]() + +
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..990f59b --- /dev/null +++ b/docs/guide/port.md @@ -0,0 +1,279 @@ + +
+ +[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`. + +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) -> Result<(), uutils_args::Error> { + match arg { + Arg::A => self.a = true, + } + Ok(()) + } +} + +let a = Settings::default().parse(std::env::args_os()).unwrap().0.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) -> Result<(), uutils_args::Error> { + match arg { + Arg::A => self.a = false, + } + Ok(()) + } +} + +let a = Settings::default().parse(std::env::args_os()).unwrap().0.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) -> Result<(), uutils_args::Error> { + match arg { + Arg::A => self.a += 1, + } + Ok(()) + } +} + +let a = Settings::default().parse(std::env::args_os()).unwrap().0.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) -> Result<(), uutils_args::Error> { + match arg { + Arg::A(s) => self.a = s, + } + Ok(()) + } +} + +let a = Settings::default().parse(std::env::args_os()).unwrap().0.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) -> Result<(), uutils_args::Error> { + match arg { + Arg::A(s) => self.a.push(s), + } + Ok(()) + } +} + +let a = Settings::default().parse(std::env::args_os()).unwrap().0.a; +``` + +
+ +[Previous](previous) +[Up](super) +[Next](next) + +
diff --git a/docs/guide/quick.md b/docs/guide/quick.md new file mode 100644 index 0000000..5de6afb --- /dev/null +++ b/docs/guide/quick.md @@ -0,0 +1,296 @@ + +
+ +[Previous]() +[Up](super) +[Next](next) + +
+ +# 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. + +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 { + #[arg("-f", "--force")] + Force, +} + +#[derive(Default)] +struct Settings { + force: bool +} + +impl Options for Settings { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { + match arg { + Arg::Force => self.force = true, + } + Ok(()) + } +} + +let (settings, operands) = Settings::default().parse(["test"]).unwrap(); +assert!(!settings.force); +assert_eq!(operands, Vec::::new()); + +let (settings, operands) = Settings::default().parse(["test", "-f"]).unwrap(); +assert!(settings.force); + +let (settings, operands) = Settings::default().parse(["test", "foo"]).unwrap(); +assert!(!settings.force); +assert_eq!(operands, vec![OsString::from("foo")]); +``` + +## 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}; +use std::ffi::OsString; + +#[derive(Arguments)] +enum Arg { + #[arg("-f", "--force")] + Force, + #[arg("-F", "--no-force")] + NoForce, +} + +#[derive(Default)] +struct Settings { + force: bool +} + +impl Options for Settings { + fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { + match arg { + Arg::Force => self.force = true, + Arg::NoForce => self.force = false, + } + Ok(()) + } +} + +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"]).unwrap(); +assert!(settings.force); +assert_eq!(operands, vec!["some-operand"]); + +let (settings, operands) = Settings::default().parse(["test", "-f", "-F", "some-other-operand"]).unwrap(); +assert!(!settings.force); +assert_eq!(operands, vec!["some-other-operand"]); +``` + +## 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), +} +# +# #[derive(Default)] +# struct Settings { +# name: OsString +# } +# +# impl Options for Settings { +# fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { +# match arg { +# Arg::Name(name) => self.name = name, +# } +# Ok(()) +# } +# } +# +# assert_eq!( +# Settings::default().parse(["test"]).unwrap().0.name, +# OsString::new(), +# ); +# assert_eq!( +# Settings::default().parse(["test", "--name=John"]).unwrap().0.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) -> Result<(), uutils_args::Error> { +# match arg { +# Arg::Name(name) => self.name = name, +# } +# Ok(()) +# } +# } +# +# assert_eq!( +# Settings::default().parse(["test", "--name"]).unwrap().0.name, +# OsString::from("anonymous"), +# ); +# assert_eq!( +# Settings::default().parse(["test", "--name=John"]).unwrap().0.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)] +# struct Settings { +# force: bool +# } +# +# impl Options for Settings { +# fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { +# match arg { +# Arg::Force(b) => self.force = b, +# } +# Ok(()) +# } +# } +# +# 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: + +> **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)] +# struct Settings { +# sort: String +# } +# +# impl Options for Settings { +# fn apply(&mut self, arg: Arg) -> Result<(), uutils_args::Error> { +# match arg { +# Arg::Sort(s) => self.sort = s, +# } +# Ok(()) +# } +# } +# +# 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")); +``` + +
+ +[Previous]() +[Up](super) +[Next](next) + +
diff --git a/docs/guide/value.md b/docs/guide/value.md new file mode 100644 index 0000000..443a960 --- /dev/null +++ b/docs/guide/value.md @@ -0,0 +1,92 @@ + +
+ +[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. + +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); +``` + +
+ +[Previous](previous) +[Up](super) +[Next](next) + +
\ No newline at end of file diff --git a/examples/completion.rs b/examples/completion.rs new file mode 100644 index 0000000..f9e4cce --- /dev/null +++ b/examples/completion.rs @@ -0,0 +1,43 @@ +#![allow(dead_code)] +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(#[allow(unused)] Number), + + // Completion is derived from the `PathBuf` type + /// Give it a path! + #[arg("-p P", "--path=P")] + Path(#[allow(unused)] PathBuf), +} + +struct Settings; + +impl Options for Settings { + fn apply(&mut self, _arg: Arg) -> Result<(), uutils_args::Error> { + panic!("Compile with the 'parse-is-complete' feature!") + } +} + +fn main() { + Settings.parse(std::env::args_os()).unwrap(); +} diff --git a/examples/deprecated.rs b/examples/deprecated.rs index 0e5cdde..71db541 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('-')?; @@ -20,31 +20,41 @@ 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), } -#[derive(Initial)] +#[derive(Default)] struct Settings { n1: usize, n2: isize, } 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(()) } } 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"]).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 d37425c..8df6742 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -1,44 +1,45 @@ -use uutils_args::{Arguments, Initial, Options}; +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! - #[option("-n NAME", "--name=NAME", "name=NAME")] + /// The name to greet + #[arg("-n NAME", "--name=NAME", "name=NAME")] Name(String), - /// The **number of times** to `greet` - #[option("-c N", "--count=N")] + /// The number of times to greet + #[arg("-c N", "--count=N")] Count(u8), /// This argument is hidden - #[option("--hidden", hidden)] + #[arg("--hidden", hidden)] Hidden, } -#[derive(Initial)] struct Settings { name: String, - #[initial(1)] count: u8, } 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(()) } } fn main() -> Result<(), uutils_args::Error> { - let settings = Settings::parse(std::env::args_os()); + let (settings, _operands) = Settings { + name: String::new(), + count: 1, + } + .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 new file mode 100644 index 0000000..67926a1 --- /dev/null +++ b/examples/value.rs @@ -0,0 +1,39 @@ +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) -> Result<(), uutils_args::Error> { + match arg { + Arg::Color(c) => self.color = c, + } + Ok(()) + } +} + +fn main() { + let (settings, _operands) = Settings::default().parse(std::env::args_os()).unwrap(); + println!("{:?}", settings.color); +} 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" + ] +} diff --git a/src/complete/bash.rs b/src/complete/bash.rs new file mode 100644 index 0000000..d69eb8b --- /dev/null +++ b/src/complete/bash.rs @@ -0,0 +1,90 @@ +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use crate::complete::{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(); + // 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_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 { + 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::complete::{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" + ) + } + + #[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" + ) + } +} diff --git a/src/complete/fish.rs b/src/complete/fish.rs new file mode 100644 index 0000000..d718e81 --- /dev/null +++ b/src/complete/fish.rs @@ -0,0 +1,122 @@ +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use crate::complete::{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 Flag { flag, .. } in &arg.short { + line.push_str(&format!(" -s {flag}")); + } + 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 { + 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!(" -f -a \"{joined}\"") + } + 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::complete::{Arg, Command, Flag, Value, ValueHint}; + + #[test] + fn short() { + let c = Command { + name: "test", + args: vec![Arg { + short: vec![Flag { + flag: "a", + value: Value::No, + }], + help: "some flag", + ..Arg::default() + }], + ..Command::default() + }; + assert_eq!(render(&c), "complete -c test -s a -d 'some flag'\n",) + } + + #[test] + fn long() { + let c = Command { + name: "test", + args: vec![Arg { + long: vec![Flag { + flag: "all", + value: Value::No, + }], + help: "some flag", + ..Arg::default() + }], + ..Command::default() + }; + 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", + args: vec![Arg { + short: vec![Flag { + flag: "a", + value: Value::No, + }], + long: vec![], + help: "some flag", + value: Some(hint), + }], + ..Command::default() + }; + assert_eq!( + render(&c), + format!("complete -c test -s a -d 'some flag' {expected}\n") + ) + } + } +} diff --git a/src/complete/man.rs b/src/complete/man.rs new file mode 100644 index 0000000..d4befed --- /dev/null +++ b/src/complete/man.rs @@ -0,0 +1,68 @@ +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use crate::complete::{Command, Flag, Value}; +use roff::{Roff, bold, italic, roman}; + +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 Flag { flag, value } in &arg.long { + if !flags.is_empty() { + flags.push(roman(", ")); + } + 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 Flag { flag, value } in &arg.short { + if !flags.is_empty() { + flags.push(roman(", ")); + } + 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.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/src/complete/md.rs b/src/complete/md.rs new file mode 100644 index 0000000..883c5b5 --- /dev/null +++ b/src/complete/md.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::complete::{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("\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 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 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(", ")); + out.push_str("
\n"); + out.push_str(&format!("
\n\n{}\n\n
\n", arg.help)); + } + out.push_str("
"); + out +} diff --git a/src/complete/mod.rs b/src/complete/mod.rs new file mode 100644 index 0000000..cfad348 --- /dev/null +++ b/src/complete/mod.rs @@ -0,0 +1,87 @@ +// 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 bash; +mod fish; +mod man; +mod md; +mod nu; +mod zsh; + +/// A description of a CLI command +/// +/// The completions and documentation will be generated based on this struct. +#[derive(Default)] +pub struct Command<'a> { + pub name: &'a str, + pub summary: &'a str, + pub version: &'a str, + pub after_options: &'a str, + pub args: Vec>, + pub license: &'a str, + pub authors: &'a str, +} + +/// 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<'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), + Unknown, + AnyPath, + FilePath, + DirPath, + ExecutablePath, + Username, + Hostname, +} + +pub fn render(c: &Command, shell: &str) -> String { + match shell { + "md" => md::render(c), + "fish" => fish::render(c), + "zsh" => zsh::render(c), + "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\"" + ), + } +} diff --git a/src/complete/nu.rs b/src/complete/nu.rs new file mode 100644 index 0000000..e7f0271 --- /dev/null +++ b/src/complete/nu.rs @@ -0,0 +1,85 @@ +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use crate::complete::{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 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 { + format!(": string{hint}") + } else { + "".into() + }; + args.push((format!("-{flag}{value}"), arg.help)); + } + for Flag { flag, value } in &arg.long { + let value = if let Value::Required(_) | Value::Optional(_) = value { + format!(": string{hint}") + } else { + "".into() + }; + args.push((format!("--{flag}{value}"), arg.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: 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, complete_commands: &str, args: &str) -> String { + format!("{complete_commands}\n\nexport extern \"{name}\" [\n{args}]\n") +} diff --git a/src/complete/zsh.rs b/src/complete/zsh.rs new file mode 100644 index 0000000..68f9d53 --- /dev/null +++ b/src/complete/zsh.rs @@ -0,0 +1,98 @@ +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use crate::complete::{Arg, Command, Flag, Value, ValueHint}; + +/// Create completion script for `zsh` +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); + + // 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; + 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, 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!( + "\ +#compdef {name} + +autoload -U is-at-least + +_{name}() {{ + typeset -A opt_args + 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 + _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" + ) +} diff --git a/src/docs.rs b/src/docs.rs new file mode 100644 index 0000000..045ef04 --- /dev/null +++ b/src/docs.rs @@ -0,0 +1,34 @@ +//! 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 { + 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")] +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/error.rs b/src/error.rs index 67593fc..2c168fa 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,11 +1,19 @@ +// 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, 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, @@ -15,10 +23,12 @@ pub enum Error { MissingPositionalArguments(Vec), /// An unrecognized option was passed. - UnexpectedOption(String), + /// + /// The second argument is a list of suggestions + 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 { @@ -46,49 +56,65 @@ 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 { + std::fmt::Display::fmt(&self.kind, 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 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: ")?; 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) => { - write!(f, "Found an invalid option '{opt}'.") + 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) => { - write!(f, "Found an invalid argument '{}'.", arg.to_string_lossy()) + ErrorKind::UnexpectedArgument(arg) => { + write!(f, "Found an invalid argument '{}'.", arg) } - 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, @@ -101,7 +127,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:" @@ -111,20 +137,22 @@ 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), - lexopt::Error::UnexpectedArgument(s) => Self::UnexpectedArgument(s), + lexopt::Error::UnexpectedOption(s) => Self::UnexpectedOption(s, Vec::new()), + 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/internal.rs b/src/internal.rs new file mode 100644 index 0000000..b74dedd --- /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 crate::error::ErrorKind; +use crate::value::Value; +use std::{ + ffi::{OsStr, OsString}, + fmt::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| ErrorKind::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, ErrorKind> { + 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(ErrorKind::UnexpectedOption( + format!("--{input}"), + filter_suggestions(input, long_options, "--"), + )), + (None, _) => Err(ErrorKind::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, +) { + let indent = " ".repeat(indent_size); + writeln!(w, "\nOptions:").unwrap(); + for (flags, help_string) in options { + let mut help_lines = help_string.lines(); + write!(w, "{}{}", &indent, &flags).unwrap(); + + if flags.len() <= width { + let line = match help_lines.next() { + Some(line) => line, + None => { + writeln!(w).unwrap(); + continue; + } + }; + let help_indent = " ".repeat(width - flags.len() + 2); + writeln!(w, "{}{}", help_indent, line).unwrap(); + } else { + writeln!(w).unwrap(); + } + + let help_indent = " ".repeat(width + indent_size + 2); + for line in help_lines { + writeln!(w, "{}{}", help_indent, line).unwrap(); + } + } +} + +#[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 15fa52a..5a0be4f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,156 +1,63 @@ -//! 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. +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +//!
//! -//! # Examples +//! [Click here for the guide](docs::guide) //! -//! 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")] +pub mod complete; mod error; +pub mod internal; +pub mod positional; mod value; -pub use derive::*; +#[cfg(doc)] +pub mod docs; + pub use lexopt; -pub use error::Error; +// 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}; -use std::{ - ffi::{OsStr, OsString}, - marker::PhantomData, -}; +use std::{ffi::OsString, marker::PhantomData}; /// A wrapper around a type implementing [`Arguments`] that adds `Help` /// and `Version` variants. @@ -158,158 +65,95 @@ use std::{ pub enum Argument { Help, Version, + Positional(OsString), + MultiPositional(Vec), Custom(T), } -fn exit_if_err(res: Result, exit_code: i32) -> T { - match res { - Ok(v) => v, - Err(err) => { - eprintln!("{err}"); - std::process::exit(exit_code); - } - } -} - /// 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 +/// [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. const EXIT_CODE: i32; - /// Parse an iterator of arguments into an - /// [`ArgumentIter`](ArgumentIter). - fn parse(args: I) -> ArgumentIter - where - I: IntoIterator + 'static, - 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, - positional_idx: &mut usize, - ) -> Result>, Error>; - - /// 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. /// /// 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; - /// 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 + 'static, - I::Item: Into, - { - exit_if_err(Self::try_check(args), Self::EXIT_CODE) - } - /// 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 + 'static, + 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(()) } + + fn complete() -> complete::Command<'static>; } /// 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`]. -pub struct ArgumentIter { +struct ArgumentIter { parser: lexopt::Parser, - pub positional_idx: usize, + positional_arguments: Vec, t: PhantomData, } impl ArgumentIter { fn from_args(args: I) -> Self where - I: IntoIterator + 'static, + I: IntoIterator, I::Item: Into, { 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)? { + 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()?; + print!("{}", T::help(self.parser.bin_name().unwrap())); std::process::exit(0); } Argument::Version => { - print!("{}", self.version()); + print!("{}", T::version()); std::process::exit(0); } - Argument::Custom(arg) => Ok(Some(arg)), + Argument::Positional(arg) => { + self.positional_arguments.push(arg); + } + Argument::MultiPositional(args) => { + self.positional_arguments.extend(args); + } + Argument::Custom(arg) => return Ok(Some(arg)), } - } else { - Ok(None) } - } - - fn help(&self) -> std::io::Result<()> { - T::help(self.parser.bin_name().unwrap()) - } - - fn version(&self) -> String { - T::version() + Ok(None) } } -/// 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: @@ -318,111 +162,58 @@ pub trait Initial: Sized { /// - the [`apply`](Options::apply) method, which defines to how map that /// 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`] -/// on the result until the arguments are exhausted, -/// 3. and finally call [`Arguments::check_missing`]. -pub trait Options: Sized + Initial { +/// 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); + fn apply(&mut self, arg: Arg) -> Result<(), Error>; - /// Parse an iterator of arguments into - fn parse(args: I) -> Self + /// Parse an iterator of arguments into the options + #[allow(unused_mut)] + fn parse(mut self, args: I) -> Result<(Self, Vec), Error> where - I: IntoIterator + 'static, + I: IntoIterator, I::Item: Into, { - exit_if_err(Self::try_parse(args), Arg::EXIT_CODE) - } + // 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_complete::<_, Self, Arg>(args.into_iter()); + std::process::exit(0); + } - fn try_parse(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); + #[cfg(not(feature = "parse-is-complete"))] + { + let mut iter = ArgumentIter::::from_args(args); + while let Some(arg) = iter.next_arg()? { + self.apply(arg)?; + } + Ok((self, iter.positional_arguments)) } - Arg::check_missing(iter.positional_idx)?; - Ok(_self) } -} -/// 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 + fn complete(shell: &str) -> String { + complete::render(&Arg::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, - }) -} - -#[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'])); - } +#[cfg(feature = "parse-is-complete")] +fn print_complete, Arg: Arguments>(mut args: I) +where + I: Iterator, + 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)); } diff --git a/src/positional.rs b/src/positional.rs new file mode 100644 index 0000000..b8b0640 --- /dev/null +++ b/src/positional.rs @@ -0,0 +1,361 @@ +//! Parsing of positional arguments. +//! +//! 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!["one"]).unwrap(); +//! assert_eq!(a, "one"); +//! assert_eq!(b, None); +//! +//! 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: +//! +//! ```ignore +//! () // no arguments +//! "FOO" // one required argument with output `OsString` +//! Opt("FOO") // one optional argument with output `Option` +//! 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", Many0("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 +//! (Many0("FOO"), Many0("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::fmt::Debug; + +/// A required argument +type Req = &'static str; + +/// Makes it's argument optional +pub struct Opt(pub T); + +/// 1 or more arguments +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; + fn unpack(&self, operands: Vec) -> Result, Error>; +} + +impl Unpack for () { + type Output = (); + + fn unpack(&self, operands: Vec) -> Result, Error> { + assert_empty(operands) + } +} + +impl Unpack for (U,) { + type Output = U::Output; + + fn unpack(&self, operands: Vec) -> Result, Error> { + self.0.unpack(operands) + } +} + +impl Unpack for Req { + type Output = T; + + fn unpack(&self, mut operands: Vec) -> Result, Error> { + let arg = pop_front(self, &mut operands)?; + assert_empty(operands)?; + Ok(arg) + } +} + +impl Unpack for Opt { + type Output = Option>; + + fn unpack(&self, operands: Vec) -> Result, Error> { + Ok(if operands.is_empty() { + None + } else { + Some(self.0.unpack(operands)?) + }) + } +} + +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, Error> { + if operands.is_empty() { + return Err(Error { + exit_code: 1, + kind: ErrorKind::MissingPositionalArguments(vec![self.0.into()]), + }); + } + Ok(operands) + } +} + +impl Unpack for (Req, U) { + type Output = (T, U::Output); + + 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, U) { + type Output = (T, T, U::Output); + + 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)?; + Ok((arg1, arg2, rest)) + } +} + +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, Error> { + let arg = pop_back(self.1, &mut operands)?; + let rest = self.0.unpack(operands)?; + Ok((rest, arg)) + } +} + +impl Unpack for (Many1, Req) { + type Output = (Vec, 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)) + } +} + +fn pop_front(name: &str, operands: &mut Vec) -> Result { + if operands.is_empty() { + return Err(Error { + exit_code: 1, + kind: ErrorKind::MissingPositionalArguments(vec![name.to_string()]), + }); + } + 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.to_string()]), + }) +} + +fn assert_empty(mut operands: Vec) -> Result<(), Error> { + if let Some(arg) = operands.pop() { + return Err(Error { + exit_code: 1, + kind: ErrorKind::UnexpectedArgument(format!("{:?}", arg)), + }); + } + Ok(()) +} + +#[cfg(test)] +mod test { + use super::{Many0, Many1, Opt, Unpack}; + + macro_rules! a { + ($e:expr, $t:ty) => { + let _: Result<$t, _> = $e.unpack(Vec::<&str>::new()); + }; + } + + #[track_caller] + 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, + { + assert_eq!(signature.unpack(Vec::from(operands)).unwrap(), expected); + } + + #[track_caller] + fn assert_err(signature: &impl Unpack, operands: [&str; N]) { + let operands = Vec::from(operands); + assert!(signature.unpack(operands).is_err()); + } + + #[test] + fn compile_tests() { + // The five basic ones + a!((), ()); + 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"), (&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<&str>, &str)); + a!((Many0("FOO"), "BAR"), (Vec<&str>, &str)); + a!((Many1("FOO"), "BAR"), (Vec<&str>, &str)); + + // Longer tuples! + 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<(&str, Option<&str>)>, &str) + ); + + // mknod NAME TYPE [MAJOR MINOR] + a!( + ("NAME", "TYPE", Opt(("MAJOR", "MINOR"))), + (&str, &str, Option<(&str, &str)>) + ); + + // chroot + a!( + ("NEWROOT", Opt(("COMMAND", Many0("ARG")))), + (&str, Option<(&str, Vec<&str>)>) + ); + } + + #[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", ["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"), ["foo"]); + assert_err(&s, ["foo", "bar"]); + assert_err(&s, ["foo", "bar", "baz"]); + } + + #[test] + fn many1() { + let s = Many1("FOO"); + assert_err(&s, []); + 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 many0() { + let s = Many0("FOO"); + assert_ok(&s, vec![], []); + 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 req_req() { + let s = ("FOO", "BAR"); + assert_err(&s, []); + assert_err(&s, ["foo"]); + 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"]); + } +} diff --git a/src/value.rs b/src/value.rs index e891d0b..c3efc1d 100644 --- a/src/value.rs +++ b/src/value.rs @@ -1,4 +1,8 @@ -use crate::error::Error; +// 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, @@ -45,9 +49,13 @@ 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; + + fn value_hint() -> ValueHint { + ValueHint::Unknown + } } impl Value for OsString { @@ -60,13 +68,21 @@ impl Value for PathBuf { fn from_value(value: &OsStr) -> ValueResult { Ok(PathBuf::from(value)) } + + fn value_hint() -> ValueHint { + ValueHint::AnyPath + } } 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()), } } } @@ -78,6 +94,10 @@ where fn from_value(value: &OsStr) -> ValueResult { Ok(Some(T::from_value(value)?)) } + + fn value_hint() -> ValueHint { + T::value_hint() + } } macro_rules! value_int { diff --git a/tests/coreutils.rs b/tests/coreutils.rs index cce04e9..94ac4fc 100644 --- a/tests/coreutils.rs +++ b/tests/coreutils.rs @@ -13,9 +13,18 @@ mod basename; #[path = "coreutils/cat.rs"] mod cat; +#[path = "coreutils/cksum.rs"] +mod cksum; + +#[path = "coreutils/date.rs"] +mod date; + #[path = "coreutils/dd.rs"] mod dd; +#[path = "coreutils/du.rs"] +mod du; + #[path = "coreutils/echo.rs"] mod echo; @@ -30,3 +39,6 @@ mod ls; #[path = "coreutils/tail.rs"] mod tail; + +#[path = "coreutils/shuf.rs"] +mod shuf; diff --git a/tests/coreutils/arch.rs b/tests/coreutils/arch.rs index d3198d3..266505d 100644 --- a/tests/coreutils/arch.rs +++ b/tests/coreutils/arch.rs @@ -5,12 +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::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 52c8036..a2081e3 100644 --- a/tests/coreutils/b2sum.rs +++ b/tests/coreutils/b2sum.rs @@ -1,34 +1,31 @@ -use std::path::{Path, PathBuf}; -use uutils_args::{Arguments, Initial, Options}; +use std::ffi::OsString; +use uutils_args::{Arguments, 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(..)] - File(PathBuf), } #[derive(Default, Debug, PartialEq, Eq)] @@ -39,18 +36,17 @@ enum CheckOutput { Status, } -#[derive(Initial)] +#[derive(Default)] struct Settings { binary: bool, check: bool, tag: bool, check_output: CheckOutput, strict: bool, - files: Vec, } 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, @@ -60,55 +56,119 @@ 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), } + Ok(()) } } #[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"]).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", "--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 + ); } #[test] fn check_output() { assert_eq!( - Settings::parse(["b2sum", "--warn"]).check_output, + Settings::default() + .parse(["b2sum", "--warn"]) + .unwrap() + .0 + .check_output, CheckOutput::Warn ); assert_eq!( - Settings::parse(["b2sum", "--quiet"]).check_output, + Settings::default() + .parse(["b2sum", "--quiet"]) + .unwrap() + .0 + .check_output, CheckOutput::Quiet ); assert_eq!( - Settings::parse(["b2sum", "--status"]).check_output, + Settings::default() + .parse(["b2sum", "--status"]) + .unwrap() + .0 + .check_output, CheckOutput::Status ); assert_eq!( - Settings::parse(["b2sum", "--status", "--warn"]).check_output, + Settings::default() + .parse(["b2sum", "--status", "--warn"]) + .unwrap() + .0 + .check_output, CheckOutput::Warn ); assert_eq!( - Settings::parse(["b2sum", "--status", "--warn"]).check_output, + Settings::default() + .parse(["b2sum", "--status", "--warn"]) + .unwrap() + .0 + .check_output, CheckOutput::Warn ); assert_eq!( - Settings::parse(["b2sum", "--warn", "--quiet"]).check_output, + Settings::default() + .parse(["b2sum", "--warn", "--quiet"]) + .unwrap() + .0 + .check_output, CheckOutput::Quiet ); assert_eq!( - Settings::parse(["b2sum", "--quiet", "--status"]).check_output, + Settings::default() + .parse(["b2sum", "--quiet", "--status"]) + .unwrap() + .0 + .check_output, CheckOutput::Status ); } @@ -116,7 +176,10 @@ fn check_output() { #[test] fn files() { assert_eq!( - Settings::parse(["b2sum", "foo", "bar"]).files, - vec![Path::new("foo"), Path::new("bar")] + 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 240d9d0..25540cf 100644 --- a/tests/coreutils/base32.rs +++ b/tests/coreutils/base32.rs @@ -1,47 +1,73 @@ -use std::path::PathBuf; +use std::ffi::OsString; -use uutils_args::{Arguments, Initial, Options}; +use uutils_args::{ + Arguments, Options, + positional::{Opt, Unpack}, +}; #[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)] - 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(), + } + } } 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), - Arg::File(f) => self.file = Some(f), } + Ok(()) } } +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!(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!(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!( + parse(["base32", "file"]).unwrap().1, + Some(OsString::from("file")) + ); } diff --git a/tests/coreutils/basename.rs b/tests/coreutils/basename.rs index 231a9fc..730c2ff 100644 --- a/tests/coreutils/basename.rs +++ b/tests/coreutils/basename.rs @@ -1,30 +1,32 @@ -use uutils_args::{Arguments, Initial, Options}; +use std::ffi::OsString; + +use uutils_args::{ + Arguments, Options, + positional::{Many1, Unpack}, +}; #[derive(Clone, Arguments)] enum Arg { - #[option("-a", "--multiple")] + #[arg("-a", "--multiple")] Multiple, - #[option("-s SUFFIX", "--suffix=SUFFIX")] - Suffix(String), + #[arg("-s SUFFIX", "--suffix=SUFFIX")] + Suffix(OsString), - #[option("-z", "--zero")] + #[arg("-z", "--zero")] Zero, - - #[positional(last, ..)] - Names(Vec), } -#[derive(Initial)] +#[derive(Default)] struct Settings { multiple: bool, - suffix: String, + suffix: OsString, zero: bool, - names: Vec, + names: Vec, } 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) => { @@ -32,23 +34,29 @@ impl Options for Settings { self.suffix = s } Arg::Zero => self.zero = true, - Arg::Names(names) => self.names = names, } + Ok(()) } } -fn parse(args: &'static [&'static str]) -> Settings { - let mut settings = Settings::parse(args); - 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"); @@ -56,7 +64,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"); @@ -64,7 +72,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"]); @@ -73,7 +81,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/cat.rs b/tests/coreutils/cat.rs index d4c9928..8b8da96 100644 --- a/tests/coreutils/cat.rs +++ b/tests/coreutils/cat.rs @@ -1,6 +1,4 @@ -use std::path::PathBuf; - -use uutils_args::{Arguments, Initial, Options}; +use uutils_args::{Arguments, Options}; #[derive(Default)] enum NumberingMode { @@ -12,49 +10,45 @@ 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(..)] - File(PathBuf), } -#[derive(Initial)] +#[derive(Default)] struct Settings { show_tabs: bool, show_ends: bool, show_nonprinting: bool, number: NumberingMode, squeeze_blank: bool, - files: Vec, } 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; @@ -75,34 +69,34 @@ 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), } + Ok(()) } } #[test] fn show() { - let s = Settings::parse(["cat", "-v"]); + let (s, _) = Settings::default().parse(["cat", "-v"]).unwrap(); assert!(!s.show_ends && !s.show_tabs && s.show_nonprinting); - let s = Settings::parse(["cat", "-E"]); + let (s, _) = Settings::default().parse(["cat", "-E"]).unwrap(); assert!(s.show_ends && !s.show_tabs && !s.show_nonprinting); - let s = Settings::parse(["cat", "-T"]); + let (s, _) = Settings::default().parse(["cat", "-T"]).unwrap(); assert!(!s.show_ends && s.show_tabs && !s.show_nonprinting); - let s = Settings::parse(["cat", "-e"]); + let (s, _) = Settings::default().parse(["cat", "-e"]).unwrap(); assert!(s.show_ends && !s.show_tabs && s.show_nonprinting); - let s = Settings::parse(["cat", "-t"]); + let (s, _) = Settings::default().parse(["cat", "-t"]).unwrap(); assert!(!s.show_ends && s.show_tabs && s.show_nonprinting); - let s = Settings::parse(["cat", "-A"]); + let (s, _) = Settings::default().parse(["cat", "-A"]).unwrap(); assert!(s.show_ends && s.show_tabs && s.show_nonprinting); - let s = Settings::parse(["cat", "-te"]); + let (s, _) = Settings::default().parse(["cat", "-te"]).unwrap(); assert!(s.show_ends && s.show_tabs && s.show_nonprinting); - let s = Settings::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/cksum.rs b/tests/coreutils/cksum.rs new file mode 100644 index 0000000..b6a8086 --- /dev/null +++ b/tests/coreutils/cksum.rs @@ -0,0 +1,202 @@ +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) -> Result<(), uutils_args::Error> { + 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; + } + } + Ok(()) + } +} + +#[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. 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), + } +} diff --git a/tests/coreutils/dd.rs b/tests/coreutils/dd.rs index 723af69..32cbe1a 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 { @@ -16,53 +16,51 @@ 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")] - Cbs(usize), + #[arg("cbs=BYTES")] + Cbs(#[allow(unused)] 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")] - Conv(String), + #[arg("conv=CONVERSIONS")] + Conv(#[allow(unused)] String), - #[option("iflag=FLAGS")] - Iflag(String), + #[arg("iflag=FLAGS")] + Iflag(#[allow(unused)] String), - #[option("oflag=FLAGS")] - Oflag(String), + #[arg("oflag=FLAGS")] + Oflag(#[allow(unused)] 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,8 +72,27 @@ 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) { + 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), @@ -85,30 +102,34 @@ 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!(), } + Ok(()) } } #[test] fn empty() { - assert_eq!(Settings::try_parse(["dd"]).unwrap(), Settings::initial()) + assert_eq!( + Settings::default().parse(["dd"]).unwrap().0, + Settings::default() + ) } #[test] fn infile() { assert_eq!( - Settings::try_parse(["dd", "if=hello"]).unwrap(), + Settings::default().parse(["dd", "if=hello"]).unwrap().0, Settings { infile: Some(PathBuf::from("hello")), - ..Settings::initial() + ..Settings::default() } ) } @@ -116,10 +137,10 @@ fn infile() { #[test] fn outfile() { assert_eq!( - Settings::try_parse(["dd", "of=hello"]).unwrap(), + Settings::default().parse(["dd", "of=hello"]).unwrap().0, Settings { outfile: Some(PathBuf::from("hello")), - ..Settings::initial() + ..Settings::default() } ) } @@ -127,35 +148,41 @@ fn outfile() { #[test] fn bs() { assert_eq!( - Settings::try_parse(["dd", "ibs=1"]).unwrap(), + Settings::default().parse(["dd", "ibs=1"]).unwrap().0, Settings { ibs: 1, obs: 512, - ..Settings::initial() + ..Settings::default() } ); assert_eq!( - Settings::try_parse(["dd", "obs=1"]).unwrap(), + Settings::default().parse(["dd", "obs=1"]).unwrap().0, Settings { ibs: 512, obs: 1, - ..Settings::initial() + ..Settings::default() } ); assert_eq!( - Settings::try_parse(["dd", "ibs=10", "obs=1"]).unwrap(), + Settings::default() + .parse(["dd", "ibs=10", "obs=1"]) + .unwrap() + .0, Settings { ibs: 10, obs: 1, - ..Settings::initial() + ..Settings::default() } ); assert_eq!( - Settings::try_parse(["dd", "ibs=10", "bs=1"]).unwrap(), + Settings::default() + .parse(["dd", "ibs=10", "bs=1"]) + .unwrap() + .0, Settings { ibs: 1, obs: 1, - ..Settings::initial() + ..Settings::default() } ) } 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()), + } + ); +} diff --git a/tests/coreutils/echo.rs b/tests/coreutils/echo.rs index d42fe69..89c3fd7 100644 --- a/tests/coreutils/echo.rs +++ b/tests/coreutils/echo.rs @@ -1,40 +1,36 @@ use std::ffi::OsString; -use uutils_args::{Arguments, Initial, Options}; +use uutils_args::{Arguments, Options}; #[derive(Arguments)] #[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)] - String(Vec), } -#[derive(Initial)] +#[derive(Default)] struct Settings { trailing_newline: bool, escape: bool, - strings: Vec, } 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, - Arg::String(s) => self.strings = s, } + Ok(()) } } @@ -42,17 +38,18 @@ impl Options for Settings { // support explicitly. #[test] +#[ignore = "needs to be fixed after positional argument refactor"] fn double_hyphen() { - let s = Settings::parse(["echo", "--"]); - assert_eq!(s.strings, vec![OsString::from("--")]); + let (_, operands) = Settings::default().parse(["echo", "--"]).unwrap(); + assert_eq!(operands, vec![OsString::from("--")]); - let s = Settings::parse(["echo", "--", "-n"]); - assert_eq!(s.strings, vec![OsString::from("--"), OsString::from("-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 s = Settings::parse(["echo", "-f"]); - assert_eq!(s.strings, vec![OsString::from("-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 07e35df..90d6375 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 @@ -8,9 +8,9 @@ use uutils_args::{Arguments, Initial, 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 + 'static, + I: IntoIterator + Clone, I::Item: Into, { let mut iter = iter.into_iter(); @@ -73,34 +73,33 @@ 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)] 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(..)] - File(PathBuf), } // We need both negative and positive 0 @@ -179,18 +178,17 @@ pub enum Mode { Lines, } -#[derive(Initial)] +#[derive(Default)] struct Settings { mode: Mode, number: SigNum, // TODO: Should be a dedicated PID type verbose: bool, - inputs: Vec, zero: bool, } 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; @@ -203,69 +201,69 @@ 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), } + Ok(()) } } -fn parse_head(iter: I) -> Result +fn parse_head(iter: I) -> Result<(Settings, Vec), uutils_args::Error> where - I: IntoIterator + Clone + 'static, + I: IntoIterator + Clone, I::Item: Into, { match parse_deprecated(iter.clone()) { Some(s) => Ok(s), - None => Settings::try_parse(iter), + None => Settings::default().parse(iter), } } #[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 4a71000..4eca547 100644 --- a/tests/coreutils/ls.rs +++ b/tests/coreutils/ls.rs @@ -1,5 +1,4 @@ -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 { @@ -39,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, } } @@ -134,151 +133,148 @@ 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", value = Time::Change)] + #[arg("-u", value = 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", 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 === - #[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", 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 - #[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", value = IndicatorStyle::Slash, help = "Append slash to directories")] + #[arg("--file-type", value = IndicatorStyle::FileType, help = "Add indicators for file types")] IndicatorStyle(IndicatorStyle), /// Classify items - #[option("-F", "--classify[=WHEN]", default = When::Always)] + #[arg("-F", "--classify[=WHEN]", value = 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", value = QuotingStyle::Literal)] + #[arg("-h", "--escape", value = QuotingStyle::Escape)] + #[arg("-Q", "--quote-name", value = todo!())] QuotingStyle(QuotingStyle), /// Set the color - #[option("--color[=WHEN]", default = When::Always)] + #[arg("--color[=WHEN]", value = 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(..)] - File(PathBuf), } fn default_terminal_size() -> u16 { @@ -302,10 +298,9 @@ fn default_terminal_size() -> u16 { 80 } -#[derive(Initial, Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq)] struct Settings { format: Format, - files: Vec, sort: Sort, recursive: bool, reverse: bool, @@ -322,22 +317,50 @@ 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(), + 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) { + 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, @@ -393,18 +416,17 @@ 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), } + Ok(()) } } #[test] fn default() { assert_eq!( - Settings::parse(["ls"]), + Settings::default().parse(["ls"]).unwrap().0, Settings { format: Format::Columns, - files: Vec::new(), sort: Sort::Name, recursive: false, reverse: false, @@ -433,87 +455,95 @@ fn default() { #[test] fn color() { - let s = Settings::parse(["ls", "--color"]); + let (s, _operands) = Settings::default().parse(["ls", "--color"]).unwrap(); assert!(s.color); - let s = Settings::parse(["ls", "--color=always"]); + let (s, _operands) = Settings::default().parse(["ls", "--color=always"]).unwrap(); assert!(s.color); - let s = Settings::parse(["ls", "--color=never"]); + let (s, _operands) = Settings::default().parse(["ls", "--color=never"]).unwrap(); assert!(!s.color); } #[test] fn format() { - let s = Settings::parse(["ls", "-l"]); + let (s, _operands) = Settings::default().parse(["ls", "-l"]).unwrap(); assert_eq!(s.format, Format::Long); - let s = Settings::parse(["ls", "-m"]); + let (s, _operands) = Settings::default().parse(["ls", "-m"]).unwrap(); assert_eq!(s.format, Format::Commas); - let s = Settings::parse(["ls", "--format=across"]); + let (s, _operands) = Settings::default() + .parse(["ls", "--format=across"]) + .unwrap(); assert_eq!(s.format, Format::Across); - let s = Settings::parse(["ls", "--format=acr"]); + let (s, _operands) = Settings::default().parse(["ls", "--format=acr"]).unwrap(); assert_eq!(s.format, Format::Across); - let s = Settings::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 = Settings::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 = Settings::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 = Settings::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 = Settings::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 = Settings::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 = Settings::parse(["ls", "--time=access"]); + let (s, _operands) = Settings::default().parse(["ls", "--time=access"]).unwrap(); assert_eq!(s.time, Time::Access); - let s = Settings::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 = Settings::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 = Settings::parse(["ls", "--classify"]); + let (s, _operands) = Settings::default().parse(["ls", "--classify"]).unwrap(); assert_eq!(s.indicator_style, IndicatorStyle::Classify); - let s = Settings::parse(["ls", "--classify=always"]); + let (s, _operands) = Settings::default() + .parse(["ls", "--classify=always"]) + .unwrap(); assert_eq!(s.indicator_style, IndicatorStyle::Classify); - let s = Settings::parse(["ls", "--classify=none"]); + let (s, _operands) = Settings::default() + .parse(["ls", "--classify=none"]) + .unwrap(); assert_eq!(s.indicator_style, IndicatorStyle::None); - let s = Settings::parse(["ls", "-F"]); + let (s, _operands) = Settings::default().parse(["ls", "-F"]).unwrap(); assert_eq!(s.indicator_style, IndicatorStyle::Classify); } #[test] fn sort() { - let s = Settings::parse(["ls", "--sort=time"]); + let (s, _operands) = Settings::default().parse(["ls", "--sort=time"]).unwrap(); assert_eq!(s.sort, Sort::Time); - let s = Settings::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 b80b352..0416d3e 100644 --- a/tests/coreutils/mktemp.rs +++ b/tests/coreutils/mktemp.rs @@ -1,32 +1,35 @@ -use std::path::{Path, PathBuf}; +use std::{ + ffi::OsString, + path::{Path, PathBuf}, +}; -use uutils_args::{Arguments, Initial, Options}; +use uutils_args::{ + Arguments, Options, + positional::{Opt, Unpack}, +}; #[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]", value = ".".into())] TmpDir(PathBuf), - - #[positional(0..=1)] - Template(String), } -#[derive(Default, Initial)] +#[derive(Default)] struct Settings { directory: bool, dry_run: bool, @@ -34,11 +37,10 @@ struct Settings { tmp_dir: Option, suffix: Option, treat_as_template: bool, - template: String, } 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, @@ -46,42 +48,57 @@ 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, } + Ok(()) } } +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 = Settings::parse(["mktemp", "--suffix=hello"]); + let (s, _template) = parse(["mktemp", "--suffix=hello"]).unwrap(); assert_eq!(s.suffix.unwrap(), "hello"); - let s = Settings::parse(["mktemp", "--suffix="]); + let (s, _template) = parse(["mktemp", "--suffix="]).unwrap(); assert_eq!(s.suffix.unwrap(), ""); - let s = Settings::parse(["mktemp", "--suffix="]); + let (s, _template) = parse(["mktemp", "--suffix="]).unwrap(); assert_eq!(s.suffix.unwrap(), ""); - let s = Settings::parse(["mktemp"]); + let (s, _template) = parse(["mktemp"]).unwrap(); assert_eq!(s.suffix, None); } #[test] fn tmpdir() { - let s = Settings::parse(["mktemp", "--tmpdir"]); + let (s, _template) = parse(["mktemp", "--tmpdir"]).unwrap(); assert_eq!(s.tmp_dir.unwrap(), Path::new(".")); - let s = Settings::parse(["mktemp", "--tmpdir="]); + let (s, _template) = parse(["mktemp", "--tmpdir="]).unwrap(); assert_eq!(s.tmp_dir.unwrap(), Path::new("")); - let s = Settings::parse(["mktemp", "-p", "foo"]); + let (s, _template) = parse(["mktemp", "-p", "foo"]).unwrap(); assert_eq!(s.tmp_dir.unwrap(), Path::new("foo")); - let s = Settings::parse(["mktemp", "-pfoo"]); + let (s, _template) = parse(["mktemp", "-pfoo"]).unwrap(); assert_eq!(s.tmp_dir.unwrap(), Path::new("foo")); - let s = Settings::parse(["mktemp", "-p", ""]); + let (s, _template) = parse(["mktemp", "-p", ""]).unwrap(); assert_eq!(s.tmp_dir.unwrap(), Path::new("")); - assert!(Settings::try_parse(["mktemp", "-p"]).is_err()); + assert!(parse(["mktemp", "-p"]).is_err()); +} + +#[test] +fn too_many_arguments() { + assert!(parse(["mktemp", "foo", "bar"]).is_err()); } diff --git a/tests/coreutils/shuf.rs b/tests/coreutils/shuf.rs new file mode 100644 index 0000000..d826b9e --- /dev/null +++ b/tests/coreutils/shuf.rs @@ -0,0 +1,397 @@ +use std::{ffi::OsString, path::PathBuf}; +use uutils_args::{ + Arguments, Options, + positional::{Many0, Opt, Unpack}, +}; + +#[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) -> Result<(), uutils_args::Error> { + match arg { + Arg::Echo => self.echo = true, + Arg::Zero => self.zero = true, + } + Ok(()) + } +} + +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] +#[ignore = "exits too early"] +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() + } + ); +} diff --git a/tests/coreutils/tail.rs b/tests/coreutils/tail.rs index 244dfcb..c302547 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 @@ -8,9 +8,9 @@ use uutils_args::{Arguments, Initial, 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 + 'static, + I: IntoIterator + Clone, I::Item: Into, { let mut iter = iter.into_iter(); @@ -95,54 +95,53 @@ where return None; } - Some(Settings { - number: sig(num), - mode, - follow, - inputs: vec![input.into().into()], - ..Settings::initial() - }) + Some(( + Settings { + number: sig(num), + mode, + follow, + ..Settings::default() + }, + vec![input.into()], + )) } #[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]", value = 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, } @@ -228,7 +227,7 @@ pub enum Mode { Lines, } -#[derive(Initial)] +#[derive(Default)] struct Settings { follow: Option, max_unchanged_stats: u32, @@ -245,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; @@ -267,42 +266,42 @@ 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, } + Ok(()) } } -fn parse_tail(iter: I) -> Result +fn parse_tail(iter: I) -> Result<(Settings, Vec), uutils_args::Error> where - I: IntoIterator + Clone + 'static, + I: IntoIterator + Clone, I::Item: Into, { match parse_deprecated(iter.clone()) { Some(s) => Ok(s), - None => Settings::try_parse(iter), + None => Settings::default().parse(iter), } } #[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 +309,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 +345,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 bf94e76..dbcce1d 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, @@ -50,7 +50,7 @@ enum Delimiters { Both, } -#[derive(Initial)] +#[derive(Default)] struct Settings { repeats_only: bool, uniques_only: bool, @@ -65,20 +65,20 @@ 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); - }, + } 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,21 @@ 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; - }, - } + } + }; + Ok(()) } -} \ No newline at end of file +} diff --git a/tests/defaults.rs b/tests/defaults.rs deleted file mode 100644 index 21ddd05..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 { - #[option("--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 { - #[option("--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/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) 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..8d4569b 100644 --- a/tests/flags.rs +++ b/tests/flags.rs @@ -1,27 +1,28 @@ -use uutils_args::{Arguments, Initial, Options}; +use uutils_args::{Arguments, Options}; #[test] fn one_flag() { #[derive(Arguments)] enum Arg { - #[option("-f", "--foo")] + #[arg("-f", "--foo")] Foo, } - #[derive(Initial)] + #[derive(Default)] struct Settings { foo: bool, } 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(()) } } - let settings = Settings::parse(["test", "-f"]); + let (settings, _) = Settings::default().parse(["test", "-f"]).unwrap(); assert!(settings.foo); } @@ -29,38 +30,42 @@ fn one_flag() { fn two_flags() { #[derive(Arguments, Clone)] enum Arg { - #[option("-a")] + #[arg("-a")] A, - #[option("-b")] + #[arg("-b")] B, } - #[derive(Initial, PartialEq, Eq, Debug)] + #[derive(Default, PartialEq, Eq, Debug)] struct Settings { a: bool, b: 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, Arg::B => self.b = true, } + Ok(()) } } assert_eq!( - Settings::parse(["test", "-a"]), + Settings::default().parse(["test", "-a"]).unwrap().0, 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"]).unwrap().0, + Settings { a: false, b: false } + ); + assert_eq!( + Settings::default().parse(["test", "-b"]).unwrap().0, Settings { a: false, b: true } ); assert_eq!( - Settings::parse(["test", "-a", "-b"]), + Settings::default().parse(["test", "-a", "-b"]).unwrap().0, Settings { a: true, b: true } ); } @@ -69,92 +74,96 @@ fn two_flags() { fn long_and_short_flag() { #[derive(Arguments)] enum Arg { - #[option("-f", "--foo")] + #[arg("-f", "--foo")] Foo, } - #[derive(Initial)] + #[derive(Default)] struct Settings { foo: bool, } 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(()) } } - assert!(!Settings::parse(["test"]).foo); - assert!(Settings::parse(["test", "--foo"]).foo); - assert!(Settings::parse(["test", "-f"]).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] fn short_alias() { #[derive(Arguments)] enum Arg { - #[option("-b")] + #[arg("-b")] Foo, } - #[derive(Initial)] + #[derive(Default)] struct Settings { foo: bool, } 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(()) } } - assert!(Settings::parse(["test", "-b"]).foo); + assert!(Settings::default().parse(["test", "-b"]).unwrap().0.foo); } #[test] fn long_alias() { #[derive(Arguments)] enum Arg { - #[option("--bar")] + #[arg("--bar")] Foo, } - #[derive(Initial)] + #[derive(Default)] struct Settings { foo: bool, } 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(()) } } - assert!(Settings::parse(["test", "--bar"]).foo); + assert!(Settings::default().parse(["test", "--bar"]).unwrap().0.foo); } #[test] fn short_and_long_alias() { #[derive(Arguments)] enum Arg { - #[option("-b", "--bar")] + #[arg("-b", "--bar")] Foo, - #[option("-f", "--foo")] + #[arg("-f", "--foo")] Bar, } - #[derive(Initial, PartialEq, Eq, Debug)] + #[derive(Default, PartialEq, Eq, Debug)] struct Settings { foo: bool, bar: bool, } 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(()) } } @@ -168,25 +177,37 @@ 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"]).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] fn xyz_map_to_abc() { #[derive(Arguments)] enum Arg { - #[option("-x")] + #[arg("-x")] X, - #[option("-y")] + #[arg("-y")] Y, - #[option("-z")] + #[arg("-z")] Z, } - #[derive(Initial, PartialEq, Eq, Debug)] + #[derive(Default, PartialEq, Eq, Debug)] struct Settings { a: bool, b: bool, @@ -194,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; @@ -210,11 +231,12 @@ fn xyz_map_to_abc() { self.c = true; } } + Ok(()) } } assert_eq!( - Settings::parse(["test", "-x"]), + Settings::default().parse(["test", "-x"]).unwrap().0, Settings { a: true, b: true, @@ -223,7 +245,7 @@ fn xyz_map_to_abc() { ); assert_eq!( - Settings::parse(["test", "-y"]), + Settings::default().parse(["test", "-y"]).unwrap().0, Settings { a: false, b: true, @@ -232,7 +254,7 @@ fn xyz_map_to_abc() { ); assert_eq!( - Settings::parse(["test", "-xy"]), + Settings::default().parse(["test", "-xy"]).unwrap().0, Settings { a: true, b: true, @@ -241,7 +263,7 @@ fn xyz_map_to_abc() { ); assert_eq!( - Settings::parse(["test", "-z"]), + Settings::default().parse(["test", "-z"]).unwrap().0, Settings { a: true, b: true, @@ -254,29 +276,33 @@ 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, } - #[derive(Initial, PartialEq, Eq, Debug)] + #[derive(Default, PartialEq, Eq, Debug)] struct Settings { a: bool, b: bool, } 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(()) } } assert_eq!( - Settings::parse(["test", "--foo-bar", "--super"]), + Settings::default() + .parse(["test", "--foo-bar", "--super"]) + .unwrap() + .0, Settings { a: true, b: true } ) } @@ -285,92 +311,128 @@ fn non_rust_ident() { fn number_flag() { #[derive(Arguments, Clone)] enum Arg { - #[option("-1")] + #[arg("-1")] One, } - #[derive(Initial, PartialEq, Eq, Debug)] + #[derive(Default, PartialEq, Eq, Debug)] struct Settings { one: bool, } 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(()) } } - assert!(Settings::parse(["test", "-1"]).one) + assert!(Settings::default().parse(["test", "-1"]).unwrap().0.one) } #[test] fn false_bool() { #[derive(Arguments)] enum Arg { - #[option("-a")] + #[arg("-a")] A, - #[option("-b")] + #[arg("-b")] B, } - #[derive(Initial)] + #[derive(Default)] struct Settings { foo: 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(()) } } - 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"]).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] fn verbosity() { #[derive(Arguments)] enum Arg { - #[option("-v")] + #[arg("-v")] Verbosity, } - #[derive(Initial)] + #[derive(Default)] struct Settings { verbosity: u8, } 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(()) } } - 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"]) + .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] 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, } - #[derive(Initial)] + #[derive(Default)] struct Settings { all: bool, almost_all: bool, @@ -378,19 +440,32 @@ 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(()) } } - 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"]).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] @@ -405,30 +480,73 @@ fn enum_flag() { #[derive(Arguments)] enum Arg { - #[option("--foo")] + #[arg("--foo")] Foo, - #[option("--bar")] + #[arg("--bar")] Bar, - #[option("--baz")] + #[arg("--baz")] Baz, } - #[derive(Initial)] + #[derive(Default)] struct Settings { foo: SomeEnum, } 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(()) + } + } + + assert_eq!( + 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"]).unwrap().0.foo, + 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(), + ), + }) } } - 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,); + 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), + } } diff --git a/tests/options.rs b/tests/options.rs index 451c43c..b9d2902 100644 --- a/tests/options.rs +++ b/tests/options.rs @@ -1,28 +1,33 @@ use std::ffi::OsStr; -use uutils_args::{Arguments, Initial, Options, Value, ValueResult}; +use uutils_args::{Arguments, Options, Value, ValueResult}; #[test] fn string_option() { #[derive(Arguments)] enum Arg { - #[option("--message=MSG")] + #[arg("--message=MSG")] Message(String), } - #[derive(Initial)] + #[derive(Default)] struct Settings { message: String, } 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(()) } } assert_eq!( - Settings::parse(["test", "--message=hello"]).message, + Settings::default() + .parse(["test", "--message=hello"]) + .unwrap() + .0 + .message, "hello" ); } @@ -42,28 +47,37 @@ fn enum_option() { #[derive(Arguments)] enum Arg { - #[option("--format=FORMAT")] + #[arg("--format=FORMAT")] Format(Format), } - #[derive(Initial)] + #[derive(Default)] struct Settings { format: Format, } 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(()) } } assert_eq!( - Settings::parse(["test", "--format=bar"]).format, + Settings::default() + .parse(["test", "--format=bar"]) + .unwrap() + .0 + .format, Format::Bar ); assert_eq!( - Settings::parse(["test", "--format", "baz"]).format, + Settings::default() + .parse(["test", "--format", "baz"]) + .unwrap() + .0 + .format, Format::Baz ); } @@ -81,27 +95,36 @@ fn enum_option_with_fields() { #[derive(Arguments)] enum Arg { - #[option("-i INDENT")] + #[arg("-i INDENT")] Indent(Indent), } - #[derive(Initial)] + #[derive(Default)] struct Settings { indent: Indent, } 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(()) } } assert_eq!( - Settings::parse(["test", "-i=thin"]).indent, + Settings::default() + .parse(["test", "-i=thin"]) + .unwrap() + .0 + .indent, Indent::Spaces(4) ); assert_eq!( - Settings::parse(["test", "-i=wide"]).indent, + Settings::default() + .parse(["test", "-i=wide"]) + .unwrap() + .0 + .indent, Indent::Spaces(8) ); } @@ -130,23 +153,38 @@ fn enum_with_complex_from_value() { #[derive(Arguments)] enum Arg { - #[option("-i INDENT")] + #[arg("-i INDENT")] Indent(Indent), } - #[derive(Initial)] + #[derive(Default)] struct Settings { indent: Indent, } 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(()) } } - 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"]) + .unwrap() + .0 + .indent, + Indent::Tabs + ); + assert_eq!( + Settings::default() + .parse(["test", "-i=4"]) + .unwrap() + .0 + .indent, + Indent::Spaces(4) + ); } #[test] @@ -164,52 +202,85 @@ fn color() { #[derive(Arguments)] enum Arg { - #[option("--color[=WHEN]")] + #[arg("--color[=WHEN]")] Color(Option), } - #[derive(Initial)] + #[derive(Default)] struct Settings { - #[initial(Color::Auto)] color: 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(()) } } assert_eq!( - Settings::parse(["test", "--color=yes"]).color, + Settings::default() + .parse(["test", "--color=yes"]) + .unwrap() + .0 + .color, Color::Always ); assert_eq!( - Settings::parse(["test", "--color=always"]).color, + Settings::default() + .parse(["test", "--color=always"]) + .unwrap() + .0 + .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"]) + .unwrap() + .0 + .color, + Color::Never + ); + assert_eq!( + Settings::default() + .parse(["test", "--color=never"]) + .unwrap() + .0 + .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"]) + .unwrap() + .0 + .color, + Color::Auto + ); + assert_eq!( + Settings::default() + .parse(["test", "--color"]) + .unwrap() + .0 + .color, + Color::Always + ) } #[test] fn actions() { #[derive(Arguments)] enum Arg { - #[option("-m MESSAGE")] + #[arg("-m MESSAGE")] Message(String), - #[option("--send")] + #[arg("--send")] Send, - #[option("--receive")] + #[arg("--receive")] Receive, } - #[derive(Initial)] + #[derive(Default)] struct Settings { last_message: String, send: bool, @@ -217,19 +288,22 @@ 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 = m.clone(); + self.last_message.clone_from(&m); self.messages.push(m); } Arg::Send => self.send = true, Arg::Receive => self.send = false, } + Ok(()) } } - let settings = Settings::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); @@ -239,61 +313,68 @@ fn actions() { fn width() { #[derive(Arguments)] enum Arg { - #[option("-w WIDTH")] + #[arg("-w WIDTH")] Width(u64), } - #[derive(Initial)] + #[derive(Default)] struct Settings { width: Option, } 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(()) } } - 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"]).unwrap().0.width, + None + ); + assert_eq!( + Settings::default().parse(["test", "-w=1"]).unwrap().0.width, + Some(1) + ); } #[test] 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), } - #[derive(Initial)] + #[derive(Default)] struct Settings { n: i128, } 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, @@ -305,21 +386,52 @@ fn integers() { Arg::I32(x) => x as i128, Arg::I64(x) => x as i128, Arg::I128(x) => x, - } + }; + Ok(()) } } - 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"]).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::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"]).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] @@ -337,69 +449,89 @@ fn ls_classify() { #[derive(Arguments)] enum Arg { - #[option( + #[arg( "-F", "--classify[=WHEN]", - default = When::Always, + value = When::Always, )] Classify(When), } - #[derive(Initial)] + #[derive(Default)] struct Settings { classify: When, } 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(()) } } - assert_eq!(Settings::parse(["test"]).classify, When::Auto); assert_eq!( - Settings::parse(["test", "--classify=never"]).classify, + 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::parse(["test", "--classify"]).classify, + Settings::default() + .parse(["test", "--classify"]) + .unwrap() + .0 + .classify, When::Always, ); - assert_eq!(Settings::parse(["test", "-F"]).classify, When::Always,); - assert!(Settings::try_parse(["test", "-Falways"]).is_err()); + assert_eq!( + Settings::default() + .parse(["test", "-F"]) + .unwrap() + .0 + .classify, + When::Always, + ); + assert!(Settings::default().parse(["test", "-Falways"]).is_err()); } #[test] fn mktemp_tmpdir() { #[derive(Clone, Arguments)] enum Arg { - #[option( + #[arg( "-p DIR", "--tmpdir[=DIR]", - default = String::from("/tmp"), + value = String::from("/tmp"), )] TmpDir(String), } - #[derive(Initial)] + #[derive(Default)] struct Settings { tmpdir: Option, } 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(()) } } - let settings = Settings::parse(["test", "-p", "X"]); + let (settings, _operands) = Settings::default().parse(["test", "-p", "X"]).unwrap(); assert_eq!(settings.tmpdir.unwrap(), "X"); - let settings = Settings::parse(["test", "--tmpdir=X"]); + let (settings, _operands) = Settings::default().parse(["test", "--tmpdir=X"]).unwrap(); assert_eq!(settings.tmpdir.unwrap(), "X"); - let settings = Settings::parse(["test", "--tmpdir"]); + let (settings, _operands) = Settings::default().parse(["test", "--tmpdir"]).unwrap(); assert_eq!(settings.tmpdir.unwrap(), "/tmp"); - assert!(Settings::try_parse(["test", "-p"]).is_err()); + assert!(Settings::default().parse(["test", "-p"]).is_err()); } #[test] @@ -445,30 +577,66 @@ fn deprecated() { #[derive(Arguments)] enum Arg { - #[free(parse_minus)] + #[arg(parse_minus)] Min(usize), - #[free(parse_plus)] + #[arg(parse_plus)] Plus(isize), } - #[derive(Initial)] + #[derive(Default)] struct Settings { n1: usize, n2: isize, } 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(()) } } - 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"]).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 + ); +} + +#[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), + } + + #[allow(dead_code)] + struct Settings {} + + impl Options for Settings { + 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 new file mode 100644 index 0000000..c7fa0e7 --- /dev/null +++ b/tests/options_first.rs @@ -0,0 +1,63 @@ +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) -> Result<(), uutils_args::Error> { + match arg { + Arg::Verbose => self.verbose = true, + } + Ok(()) + } + } + + 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")]); +} diff --git a/tests/positionals.rs b/tests/positionals.rs deleted file mode 100644 index 5e0bd3b..0000000 --- a/tests/positionals.rs +++ /dev/null @@ -1,163 +0,0 @@ -use uutils_args::{Arguments, Initial, Options}; - -#[test] -fn one_positional() { - #[derive(Arguments, Clone)] - enum Arg { - #[positional(1)] - File1(String), - } - - #[derive(Initial)] - struct Settings { - file1: String, - } - - impl Options for Settings { - fn apply(&mut self, Arg::File1(f): Arg) { - self.file1 = f; - } - } - - let settings = Settings::parse(["test", "foo"]); - assert_eq!(settings.file1, "foo"); - - assert!(Settings::try_parse(["test"]).is_err()); -} - -#[test] -fn two_positionals() { - #[derive(Arguments)] - enum Arg { - #[positional(1)] - Foo(String), - #[positional(1)] - Bar(String), - } - - #[derive(Initial)] - 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::parse(["test", "a", "b"]); - assert_eq!(settings.foo, "a"); - assert_eq!(settings.bar, "b"); - - assert!(Settings::try_parse(["test"]).is_err()); -} - -#[test] -fn optional_positional() { - #[derive(Arguments)] - enum Arg { - #[positional(0..=1)] - Foo(String), - } - - #[derive(Initial)] - struct Settings { - foo: Option, - } - - impl Options for Settings { - fn apply(&mut self, Arg::Foo(x): Arg) { - self.foo = Some(x); - } - } - - let settings = Settings::parse(["test"]); - assert_eq!(settings.foo, None); - let settings = Settings::parse(["test", "bar"]); - assert_eq!(settings.foo.unwrap(), "bar"); -} - -#[test] -fn collect_positional() { - #[derive(Arguments, Clone)] - enum Arg { - #[positional(..)] - Foo(String), - } - - #[derive(Initial)] - struct Settings { - foo: Vec, - } - - impl Options for Settings { - fn apply(&mut self, Arg::Foo(x): Arg) { - self.foo.push(x); - } - } - - let settings = Settings::parse(["test", "a", "b", "c"]); - assert_eq!(settings.foo, vec!["a", "b", "c"]); - let settings = Settings::parse(["test"]); - assert_eq!(settings.foo, Vec::::new()); -} - -#[test] -fn last1() { - #[derive(Arguments)] - enum Arg { - #[positional(last, ..)] - Foo(Vec), - } - - #[derive(Initial)] - struct Settings { - foo: Vec, - } - - impl Options for Settings { - fn apply(&mut self, Arg::Foo(x): Arg) { - self.foo = x; - } - } - - let settings = Settings::parse(["test", "a", "-b", "c"]); - assert_eq!(settings.foo, vec!["a", "-b", "c"]); -} - -#[test] -fn last2() { - #[derive(Arguments, Clone)] - enum Arg { - #[option("-a")] - A, - - #[positional(last, ..)] - Foo(Vec), - } - - #[derive(Initial)] - 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::parse(["test", "-a"]); - assert_eq!(settings.foo, Vec::::new()); - - let settings = Settings::parse(["test", "--", "-a"]); - assert_eq!(settings.foo, vec!["-a"]); -}