From d62daf91556a3e0274a463e84203f7bc574f98b5 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Wed, 13 Dec 2023 18:53:30 +0100 Subject: [PATCH 01/11] start working on guide-level docs --- derive/src/lib.rs | 15 + .../design}/arguments_in_coreutils.md | 25 +- docs/design/design.md | 1 + design/design.md => docs/design/iterative.md | 20 +- {design => docs/design}/problems_with_clap.md | 7 +- docs/guide/completions.md | 1 + docs/guide/guide.md | 12 + docs/guide/port.md | 1 + docs/guide/quick.md | 278 ++++++++++++++++++ docs/guide/value.md | 1 + design/README.md => docs/index.md | 3 +- src/docs.rs | 24 ++ src/lib.rs | 4 + 13 files changed, 374 insertions(+), 18 deletions(-) rename {design => docs/design}/arguments_in_coreutils.md (85%) create mode 100644 docs/design/design.md rename design/design.md => docs/design/iterative.md (91%) rename {design => docs/design}/problems_with_clap.md (98%) create mode 100644 docs/guide/completions.md create mode 100644 docs/guide/guide.md create mode 100644 docs/guide/port.md create mode 100644 docs/guide/quick.md create mode 100644 docs/guide/value.md rename design/README.md => docs/index.md (97%) create mode 100644 src/docs.rs diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 4842374..5a74dfe 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -18,6 +18,21 @@ use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, Data::Enum, DeriveInput}; +/// Derive `Arguments` +/// +/// ## Argument specifications +/// +/// | specification | kind | value | +/// | -------------- | ---------- | -------- | +/// | `VAL` | positional | n.a. | +/// | `-s` | short | none | +/// | `-s VAL` | short | required | +/// | `-s[VAL]` | short | optional | +/// | `--long` | long | none | +/// | `--long=VAL` | long | required | +/// | `--long[=VAL]` | long | optional | +/// | `long=VAL` | dd | required | +/// #[proc_macro_derive(Arguments, attributes(arg, arguments))] pub fn arguments(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); diff --git a/design/arguments_in_coreutils.md b/docs/design/arguments_in_coreutils.md similarity index 85% rename from design/arguments_in_coreutils.md rename to docs/design/arguments_in_coreutils.md index 818bebc..a553511 100644 --- a/design/arguments_in_coreutils.md +++ b/docs/design/arguments_in_coreutils.md @@ -109,12 +109,25 @@ Some utils take positional arguments, which might be required. ### Deprecated syntax `+N` and `-N` -Some utils (e.g. `head`, `tail`, `kill`, `fold` and `uniq`) support an old deprecated syntax where numbers can be directly passed as arguments as a shorthand. For example, `uniq +5` is a shorthand for `uniq -s 5` and `uniq -5` is short for `uniq -f 5`. +Some utils (e.g. `head`, `tail`, `kill`, `fold` and `uniq`) support an old +deprecated syntax where numbers can be directly passed as arguments as a +shorthand. For example, `uniq +5` is a shorthand for `uniq -s 5` and `uniq -5` +is short for `uniq -f 5`. These all behave slightly differently. -1. `head` and `tail` only accept this if it is the first argument and either 1 or 2 arguments are given. -2. In `fold` the `-N` must be standalone (e.g. `-10b` is rejected), but can appear at any position. -3. In `kill`, the same rules as `fold` apply, but it can also be a name instead of a number. -4. In `uniq`, the syntax does not need to stand alone and is additive in a weird way, because they hack `-22` as `-2 -2` so each flag `-1...-9` multiplies the previous by 10 and adds itself. I'm not sure that we need to support this. Doing something like what `fold` and `kill` do is probably fine. Also note that to make it extra confusing, the `+` variant works like `fold`. + +1. `head` and `tail` only accept this if it is the first argument and either 1 + or 2 arguments are given. +2. In `fold` the `-N` must be standalone (e.g. `-10b` is rejected), but can + appear at any position. +3. In `kill`, the same rules as `fold` apply, but it can also be a name instead + of a number. +4. In `uniq`, the syntax does not need to stand alone and is additive in a weird + way, because they hack `-22` as `-2 -2` so each flag `-1...-9` multiplies the + previous by 10 and adds itself. I'm not sure that we need to support this. + Doing something like what `fold` and `kill` do is probably fine. Also note + that to make it extra confusing, the `+` variant works like `fold`. 5. `pr` the behaviour is similar to `uniq`. -6. `split` seems to be somewhere between `uniq` and `fold`. It accepts things like `-x10x` correctly, but it doesn't do the additive thing from `uniq` across multiple occurrences. Basically, it's very clever and cursed. +6. `split` seems to be somewhere between `uniq` and `fold`. It accepts things + like `-x10x` correctly, but it doesn't do the additive thing from `uniq` + across multiple occurrences. Basically, it's very clever and cursed. diff --git a/docs/design/design.md b/docs/design/design.md new file mode 100644 index 0000000..3d14cb7 --- /dev/null +++ b/docs/design/design.md @@ -0,0 +1 @@ +# Design diff --git a/design/design.md b/docs/design/iterative.md similarity index 91% rename from design/design.md rename to docs/design/iterative.md index 9754b42..2fcec1a 100644 --- a/design/design.md +++ b/docs/design/iterative.md @@ -64,10 +64,10 @@ struct Settings { > is always obvious where an argument is defined. As part of the `Options` derive, we get a `Settings::parse` method that returns -a `Settings` from a `OsString` iterator. The implementation of -this is defined by the `set` and `map` attributes. `map` just says: "if we -encounter this value in the iterator set this value", using a match-like syntax -(it expands to a match). And the `#[set(Arg::Name)]` is just short for +a `Settings` from a `OsString` iterator. The implementation of this is defined +by the `set` and `map` attributes. `map` just says: "if we encounter this value +in the iterator set this value", using a match-like syntax (it expands to a +match). And the `#[set(Arg::Name)]` is just short for `#[map(Arg::Name(name) => name)]`, because that is a commonly appearing pattern. Importantly, arguments can appear in the attributes for multiple fields. We @@ -147,7 +147,9 @@ enum Arg { ## Options struct -The options struct has just one fundamental attribute: `map`. It works much like a `match` expression (in fact, that's what it expands to). Furthermore, it's possible to define defaults on fields. +The options struct has just one fundamental attribute: `map`. It works much like +a `match` expression (in fact, that's what it expands to). Furthermore, it's +possible to define defaults on fields. ```rust #[derive(Options, Default)] @@ -180,7 +182,8 @@ struct Settings { } ``` -As a shorthand, there is also a `set` attribute. These fields behave identically: +As a shorthand, there is also a `set` attribute. These fields behave +identically: ```rust #[derive(Options, Default)] @@ -195,7 +198,8 @@ struct Settings { ## `FromValue` enums -We often want to map values to some enum, we can define this mapping by deriving `FromValue`: +We often want to map values to some enum, we can define this mapping by deriving +`FromValue`: ```rust #[derive(Default, FromValue)] @@ -210,4 +214,4 @@ enum Color { #[value("never", "no", "none")] Never, } -``` \ No newline at end of file +``` diff --git a/design/problems_with_clap.md b/docs/design/problems_with_clap.md similarity index 98% rename from design/problems_with_clap.md rename to docs/design/problems_with_clap.md index 689165c..1650c18 100644 --- a/design/problems_with_clap.md +++ b/docs/design/problems_with_clap.md @@ -111,7 +111,7 @@ uutils diverge. `clap`'s arguments are identified by strings. This leads to code like this: -```rust +```rust,ignore const OPT_NAME: &'static str = "name"; // -- snip -- @@ -183,9 +183,10 @@ libraries. - Does not support a many-to-many relationship. - [`bpaf`](https://github.com/pacak/bpaf) - Extremely flexible, even supports `dd`-style. - - A different configuration between short and long options requires a workaround. + - A different configuration between short and long options requires a + workaround. - A many-to-many relation ship is possible, though not very ergonomic. - - For more information, see: https://github.com/uutils/uutils-args/issues/17 + - For more information, see: - [`gumdrop`](https://github.com/murarth/gumdrop) - Does not handle invalid UTF-8. - Not configurable enough. diff --git a/docs/guide/completions.md b/docs/guide/completions.md new file mode 100644 index 0000000..e4228de --- /dev/null +++ b/docs/guide/completions.md @@ -0,0 +1 @@ +# Completions diff --git a/docs/guide/guide.md b/docs/guide/guide.md new file mode 100644 index 0000000..fbd7a49 --- /dev/null +++ b/docs/guide/guide.md @@ -0,0 +1,12 @@ +# Guide + +This module provides guide-level documentation for [`uutils-args`](crate). If +you're unfamiliar with this library you probably want to start with the first +chapter below and work your way through. + +## Chapters + +1. [Quick Start](guide::quick) +2. [Porting a Parser from `clap`](guide::port) +3. [The `Value` trait](`guide::value`) +4. [Generating completions](`guide::completions`) diff --git a/docs/guide/port.md b/docs/guide/port.md new file mode 100644 index 0000000..c7eaa61 --- /dev/null +++ b/docs/guide/port.md @@ -0,0 +1 @@ +# Porting from Clap diff --git a/docs/guide/quick.md b/docs/guide/quick.md new file mode 100644 index 0000000..ba3ea51 --- /dev/null +++ b/docs/guide/quick.md @@ -0,0 +1,278 @@ +# Quick Start + +A parser consists of two parts: + +- an `enum` implementing [`Arguments`](crate::Arguments) +- an `struct` implementing [`Options`](crate::Options) + +The `enum` defines all the arguments that your application accepts. The `struct` represents all configuration options for the application. In other words, the `struct` is the internal representation of the options, while the `enum` is the external representation. + +## A single flag + +We can create arguments by annotating a variant of an `enum` deriving [`Arguments`](crate::Arguments) with the `arg` attribute. This attribute takes strings that define the arguments. A short flag, for instance, looks like `"-f"` and a long flag looks like `"--flag"`. The full syntax for the arguments specifications can be found in the documentation for the [`Arguments` derive macro](derive@crate::Arguments) + +To represent the program configuration we create a struct called `Settings`, which implements `Options`. When an argument is encountered, we _apply_ it to the `Settings` struct. In this case, we set the `force` field of `Settings` to `true` if `Arg::Force` is parsed. + +```rust +use uutils_args::{Arguments, Options}; + +#[derive(Arguments)] +enum Arg { + #[arg("-f", "--force")] + Force, +} + +// Note: Debug, PartialEq and Eq are only necessary for assert_eq! below. +#[derive(Default, Debug, PartialEq, Eq)] +struct Settings { + force: bool +} + +impl Options for Settings { + fn apply(&mut self, arg: Arg) { + match arg { + Arg::Force => self.force = true, + } + } +} + +assert_eq!(Settings::default().parse(["test"]), Settings { force: false }); +assert_eq!(Settings::default().parse(["test", "-f"]), Settings { force: true }); +``` + +## Two overriding flags + +Of course, we can define multiple flags. If these arguments change the same fields of `Settings`, then they will override. This is important: by default none of the arguments will "conflict", they will always simply be processed in order. + +```rust +use uutils_args::{Arguments, Options}; + +#[derive(Arguments)] +enum Arg { + #[arg("-f", "--force")] + Force, + #[arg("-F", "--no-force")] + NoForce, +} + +// Note: Debug, PartialEq and Eq are only necessary for assert_eq! below. +#[derive(Default, Debug, PartialEq, Eq)] +struct Settings { + force: bool +} + +impl Options for Settings { + fn apply(&mut self, arg: Arg) { + match arg { + Arg::Force => self.force = true, + Arg::NoForce => self.force = false, + } + } +} + +assert_eq!(Settings::default().parse(["test"]), Settings { force: false }); +assert_eq!(Settings::default().parse(["test", "-f"]), Settings { force: true }); +assert_eq!(Settings::default().parse(["test", "-F"]), Settings { force: false }); +``` + +## Help strings + +We can document our flags in two ways: by giving them a docstring or by giving the `arg` attribute a `help` argument. Note that the `help` argument will take precedence over the docstring. + +```rust +use uutils_args::Arguments; + +#[derive(Arguments)] +enum Arg { + /// Force! + #[arg("-f", "--force")] + Force, + #[arg("-F", "--no-force", help = "No! Don't force!")] + NoForce, +} +``` + +## Arguments with required values + +So far, our arguments have been simple flags that do not take any arguments, but `uutils-args` supports much more! If we want an argument for our option, the corresponding variant on our `enum` needs to take an argument too. + +> **Note**: In the example below, we use `OsString`. A regular `String` works too, but is generally discouraged in `coreutils`, because we often have to support text with invalid UTF-8. + +```rust +# use uutils_args::{Arguments, Options}; +# use std::ffi::OsString; +# +#[derive(Arguments)] +enum Arg { + #[arg("-n NAME", "--name=NAME")] + Name(OsString), +} +# +# // Note: Debug, PartialEq and Eq are only necessary for assert_eq! below. +# #[derive(Default, Debug, PartialEq, Eq)] +# struct Settings { +# name: OsString +# } +# +# impl Options for Settings { +# fn apply(&mut self, arg: Arg) { +# match arg { +# Arg::Name(name) => self.name = name, +# } +# } +# } +# +# assert_eq!( +# Settings::default().parse(["test"]), +# Settings { name: OsString::new() } +# ); +# assert_eq!( +# Settings::default().parse(["test", "--name=John"]), +# Settings { name: OsString::from("John")} +# ); +``` + +## Arguments with optional values + +Arguments with optional values are possible, too. However, we have to give a value to be used if the value is not given. Below, we set that value to `OsString::from("anonymous")`, with the `value` argument of `arg`. + +```rust +# use uutils_args::{Arguments, Options}; +# use std::ffi::OsString; +# +#[derive(Arguments)] +enum Arg { + #[arg("-n[NAME]", "--name[=NAME]", value = OsString::from("anonymous"))] + Name(OsString), +} +# +# #[derive(Default, Debug, PartialEq, Eq)] +# struct Settings { +# name: OsString +# } +# +# impl Options for Settings { +# fn apply(&mut self, arg: Arg) { +# match arg { +# Arg::Name(name) => self.name = name, +# } +# } +# } +# +# assert_eq!( +# Settings::default().parse(["test", "--name"]), +# Settings { name: OsString::from("anonymous")} +# ); +# assert_eq!( +# Settings::default().parse(["test", "--name=John"]), +# Settings { name: OsString::from("John")} +# ); +``` + +## Multiple arguments per variant + +Here's a neat trick: you can use multiple `arg` attributes per variant. Recall the `--force/--no-force` example above. We could have written that as follows: + +```rust +# use uutils_args::{Arguments, Options}; +# +#[derive(Arguments)] +enum Arg { + #[arg("-f", "--force", value = true, help = "enable force")] + #[arg("-F", "--no-force", value = false, help = "disable force")] + Force(bool), +} +# +# #[derive(Default, Debug, PartialEq, Eq)] +# struct Settings { +# force: bool +# } +# +# impl Options for Settings { +# fn apply(&mut self, arg: Arg) { +# match arg { +# Arg::Force(b) => self.force = b, +# } +# } +# } +# +# assert_eq!(Settings::default().parse(["test"]), Settings { force: false }); +# assert_eq!(Settings::default().parse(["test", "-f"]), Settings { force: true }); +# assert_eq!(Settings::default().parse(["test", "-F"]), Settings { force: false }); +``` + +This is particularly interesting for defining "shortcut" arguments. For example, `ls` takes a `--sort=WORD` argument, that defines how the files should be sorted. But it also has shorthands like `-t`, which is the same as `--sort=time`. All of these can be implemented on one variant: + +> **Note**: The `--sort` argument should not take a `String` as value. We've done that here for illustrative purposes. It should actually use an `enum` with the `Value` trait. + +```rust +# use uutils_args::{Arguments, Options}; +# +#[derive(Arguments)] +enum Arg { + #[arg("--sort=WORD", help = "Sort by WORD")] + #[arg("-t", value = String::from("time"), help = "Sort by time")] + #[arg("-U", value = String::from("none"), help = "Do not sort")] + #[arg("-v", value = String::from("version"), help = "Sort by version")] + #[arg("-X", value = String::from("extension"), help = "Sort by extension")] + Sort(String), +} +# +# #[derive(Default, Debug, PartialEq, Eq)] +# struct Settings { +# sort: String +# } +# +# impl Options for Settings { +# fn apply(&mut self, arg: Arg) { +# match arg { +# Arg::Sort(s) => self.sort = s, +# } +# } +# } +# +# assert_eq!(Settings::default().parse(["test"]), Settings { sort: String::new() }); +# assert_eq!(Settings::default().parse(["test", "--sort=time"]), Settings { sort: String::from("time") }); +# assert_eq!(Settings::default().parse(["test", "-t"]), Settings { sort: String::from("time") }); +``` + +## Positional arguments + +Finally, at the end of this whirlwind tour, we get to positional arguments. Here's a simple positional argument: + +```rust +use uutils_args::{Arguments, Options}; +use std::path::PathBuf; + +#[derive(Arguments)] +enum Arg { + #[arg("FILES", ..)] + File(PathBuf) +} + +#[derive(Default, Debug, PartialEq, Eq)] +struct Settings { + files: Vec, +} + +impl Options for Settings { + fn apply(&mut self, arg: Arg) { + match arg { + Arg::File(f) => self.files.push(f), + } + } +} +# +# assert_eq!( +# Settings::default().parse(["test"]), +# Settings { files: Vec::new() } +# ); +# assert_eq!( +# Settings::default().parse(["test", "foo"]), +# Settings { files: vec![PathBuf::from("foo")] } +# ); +# assert_eq!( +# Settings::default().parse(["test", "foo", "bar"]), +# Settings { files: vec!["foo".into(), "bar".into()] } +# ); +``` \ No newline at end of file diff --git a/docs/guide/value.md b/docs/guide/value.md new file mode 100644 index 0000000..4c689c8 --- /dev/null +++ b/docs/guide/value.md @@ -0,0 +1 @@ +# Value trait diff --git a/design/README.md b/docs/index.md similarity index 97% rename from design/README.md rename to docs/index.md index 164bca4..22b94bf 100644 --- a/design/README.md +++ b/docs/index.md @@ -14,7 +14,8 @@ decisions. Before diving in, let's lay out the design goals of this project. fewer features to support. - Use outside uutils is possible but not prioritized. Hence, configurability beyond the coreutils is not necessary. -- Errors must be at least as good as GNU's, but may be different (hopefully improved). +- Errors must be at least as good as GNU's, but may be different (hopefully + improved). ## Pages diff --git a/src/docs.rs b/src/docs.rs new file mode 100644 index 0000000..3168ade --- /dev/null +++ b/src/docs.rs @@ -0,0 +1,24 @@ +//! This module contains only documentation to be rendered by rustdoc. +//! +//! - [Guide](guide): the guide for using this library +//! - [Design](design): documents about the design of this library + +#[doc = include_str!("../docs/guide/guide.md")] +pub mod guide { + #[doc = include_str!("../docs/guide/quick.md")] + pub mod quick {} + #[doc = include_str!("../docs/guide/port.md")] + pub mod port {} + #[doc = include_str!("../docs/guide/completions.md")] + pub mod completions {} + #[doc = include_str!("../docs/guide/value.md")] + pub mod value {} +} + +#[doc = include_str!("../docs/design/design.md")] +pub mod design { + #[doc = include_str!("../docs/design/arguments_in_coreutils.md")] + pub mod coreutils {} + #[doc = include_str!("../docs/design/problems_with_clap.md")] + pub mod problems {} +} diff --git a/src/lib.rs b/src/lib.rs index 5f4ecc5..87716a1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,16 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +//! [Click here to check out the guide-level documentation](docs::guide) #![doc = include_str!("../README.md")] mod error; pub mod internal; mod value; +#[cfg(doc)] +pub mod docs; + pub use lexopt; pub use uutils_args_derive::*; From 757214b6bebec4e7da77ea525b1219c437fc0538 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Wed, 13 Dec 2023 19:01:26 +0100 Subject: [PATCH 02/11] do not require value_hint when none of the arguments use it --- derive/src/complete.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/derive/src/complete.rs b/derive/src/complete.rs index d7a77e4..10b9691 100644 --- a/derive/src/complete.rs +++ b/derive/src/complete.rs @@ -38,6 +38,12 @@ pub fn complete(args: &[Argument], file: &Option) -> TokenStream { continue; } + // If none of the flags take an argument, we won't need ValueHint + // based on that type. So we should not attempt to call `value_hint` + // on it. + let any_flag_takes_argument = + short.iter().any(|f| f.value != Value::No) && long.iter().any(|f| f.value != Value::No); + let short: Vec<_> = short .iter() .map(|Flag { flag, value }| { @@ -69,10 +75,9 @@ pub fn complete(args: &[Argument], file: &Option) -> TokenStream { }) .collect(); - let hint = if let Some(ty) = field { - quote!(Some(<#ty>::value_hint())) - } else { - quote!(None) + let hint = match (field, any_flag_takes_argument) { + (Some(ty), true) => quote!(Some(<#ty>::value_hint())), + _ => quote!(None), }; arg_specs.push(quote!( From d67f2cd250cb08c11e897dca36465551d61d692b Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Thu, 14 Dec 2023 11:39:58 +0100 Subject: [PATCH 03/11] clean up assertions for quick start --- docs/guide/quick.md | 63 +++++++++++++++++++++------------------------ 1 file changed, 30 insertions(+), 33 deletions(-) diff --git a/docs/guide/quick.md b/docs/guide/quick.md index ba3ea51..5f5a12d 100644 --- a/docs/guide/quick.md +++ b/docs/guide/quick.md @@ -22,8 +22,7 @@ enum Arg { Force, } -// Note: Debug, PartialEq and Eq are only necessary for assert_eq! below. -#[derive(Default, Debug, PartialEq, Eq)] +#[derive(Default)] struct Settings { force: bool } @@ -36,8 +35,8 @@ impl Options for Settings { } } -assert_eq!(Settings::default().parse(["test"]), Settings { force: false }); -assert_eq!(Settings::default().parse(["test", "-f"]), Settings { force: true }); +assert!(!Settings::default().parse(["test"]).force); +assert!(Settings::default().parse(["test", "-f"]).force); ``` ## Two overriding flags @@ -55,8 +54,7 @@ enum Arg { NoForce, } -// Note: Debug, PartialEq and Eq are only necessary for assert_eq! below. -#[derive(Default, Debug, PartialEq, Eq)] +#[derive(Default)] struct Settings { force: bool } @@ -70,9 +68,9 @@ impl Options for Settings { } } -assert_eq!(Settings::default().parse(["test"]), Settings { force: false }); -assert_eq!(Settings::default().parse(["test", "-f"]), Settings { force: true }); -assert_eq!(Settings::default().parse(["test", "-F"]), Settings { force: false }); +assert!(!Settings::default().parse(["test"]).force); +assert!(Settings::default().parse(["test", "-f"]).force); +assert!(!Settings::default().parse(["test", "-f", "-F"]).force); ``` ## Help strings @@ -108,8 +106,7 @@ enum Arg { Name(OsString), } # -# // Note: Debug, PartialEq and Eq are only necessary for assert_eq! below. -# #[derive(Default, Debug, PartialEq, Eq)] +# #[derive(Default)] # struct Settings { # name: OsString # } @@ -123,12 +120,12 @@ enum Arg { # } # # assert_eq!( -# Settings::default().parse(["test"]), -# Settings { name: OsString::new() } +# Settings::default().parse(["test"]).name, +# OsString::new(), # ); # assert_eq!( -# Settings::default().parse(["test", "--name=John"]), -# Settings { name: OsString::from("John")} +# Settings::default().parse(["test", "--name=John"]).name, +# OsString::from("John"), # ); ``` @@ -160,12 +157,12 @@ enum Arg { # } # # assert_eq!( -# Settings::default().parse(["test", "--name"]), -# Settings { name: OsString::from("anonymous")} +# Settings::default().parse(["test", "--name"]).name, +# OsString::from("anonymous"), # ); # assert_eq!( -# Settings::default().parse(["test", "--name=John"]), -# Settings { name: OsString::from("John")} +# Settings::default().parse(["test", "--name=John"]).name, +# OsString::from("John"), # ); ``` @@ -183,7 +180,7 @@ enum Arg { Force(bool), } # -# #[derive(Default, Debug, PartialEq, Eq)] +# #[derive(Default)] # struct Settings { # force: bool # } @@ -196,9 +193,9 @@ enum Arg { # } # } # -# assert_eq!(Settings::default().parse(["test"]), Settings { force: false }); -# assert_eq!(Settings::default().parse(["test", "-f"]), Settings { force: true }); -# assert_eq!(Settings::default().parse(["test", "-F"]), Settings { force: false }); +# assert!(!Settings::default().parse(["test"]).force); +# assert!(Settings::default().parse(["test", "-f"]).force); +# assert!(!Settings::default().parse(["test", "-F"]).force); ``` This is particularly interesting for defining "shortcut" arguments. For example, `ls` takes a `--sort=WORD` argument, that defines how the files should be sorted. But it also has shorthands like `-t`, which is the same as `--sort=time`. All of these can be implemented on one variant: @@ -218,7 +215,7 @@ enum Arg { Sort(String), } # -# #[derive(Default, Debug, PartialEq, Eq)] +# #[derive(Default)] # struct Settings { # sort: String # } @@ -231,9 +228,9 @@ enum Arg { # } # } # -# assert_eq!(Settings::default().parse(["test"]), Settings { sort: String::new() }); -# assert_eq!(Settings::default().parse(["test", "--sort=time"]), Settings { sort: String::from("time") }); -# assert_eq!(Settings::default().parse(["test", "-t"]), Settings { sort: String::from("time") }); +# assert_eq!(Settings::default().parse(["test"]).sort, String::new()); +# assert_eq!(Settings::default().parse(["test", "--sort=time"]).sort, String::from("time")); +# assert_eq!(Settings::default().parse(["test", "-t"]).sort, String::from("time")); ``` ## Positional arguments @@ -264,15 +261,15 @@ impl Options for Settings { } # # assert_eq!( -# Settings::default().parse(["test"]), -# Settings { files: Vec::new() } +# Settings::default().parse(["test"]).files, +# Vec::::new(), # ); # assert_eq!( -# Settings::default().parse(["test", "foo"]), -# Settings { files: vec![PathBuf::from("foo")] } +# Settings::default().parse(["test", "foo"]).files, +# vec![PathBuf::from("foo")], # ); # assert_eq!( -# Settings::default().parse(["test", "foo", "bar"]), -# Settings { files: vec!["foo".into(), "bar".into()] } +# Settings::default().parse(["test", "foo", "bar"]).files, +# vec![PathBuf::from("foo"), PathBuf::from("bar")], # ); ``` \ No newline at end of file From 8adaad0565c379987a10995e58f9bd7887c91b1c Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Thu, 14 Dec 2023 14:32:01 +0100 Subject: [PATCH 04/11] start writing guide for porting from clap --- docs/guide/port.md | 238 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 238 insertions(+) diff --git a/docs/guide/port.md b/docs/guide/port.md index c7eaa61..c99739a 100644 --- a/docs/guide/port.md +++ b/docs/guide/port.md @@ -1 +1,239 @@ # Porting from Clap + +This chapter contains information about how common patterns in `clap` parsers can be ported to `uutils-args`. + +More examples can be added here while we figure out more common patterns. + +## Defaults + +By default, the `clap` command roughly equivalent to a command from `uutils-args` looks like this (where everything with `...` is filled in automatically). + +```rust,ignore +Command::new(...) + .version(...) + .override_usage(...) + .about(...) + .infer_long_args(true) + .args_override_self(true) + .disable_help_flag(true) + .disable_version_flag(true) + .arg( + Arg::new("help") + .long("help") + .help("Print help information.") + .action(ArgAction::Help), + ) + .arg( + Arg::new("version") + .long("version") + .help("Print version information.") + .action(ArgAction::Version), + ) +``` + +Further differences are: + +- Overrides are the default in `uutils-args`. There is no automatic conflict checking. +- Values can always start with hyphens. +- Long flags with optional arguments always require an equal sign. + +## `ArgAction` equivalents + +### `ArgAction::SetTrue` + +```rust,ignore +let command = Command::new(...) + .arg( + Arg::new("a") + .short('a') + .action(ArgAction::SetTrue) + ); + +let matches = command.get_matches(); + +let a = matches.get_flag("a"); +``` + +```rust +use uutils_args::{Arguments, Options}; + +#[derive(Arguments)] +enum Arg { + #[arg("-a")] + A +} + +#[derive(Default)] +struct Settings { a: bool } + +impl Options for Settings { + fn apply(&mut self, arg: Arg) { + match arg { + Arg::A => self.a = true, + } + } +} + +let a = Settings::default().parse(std::env::args_os()).a; +``` + +### `ArgAction::SetFalse` + +```rust,ignore +let command = Command::new(...) + .arg( + Arg::new("a") + .short('a') + .action(ArgAction::SetFalse) + ); + +let matches = command.get_matches(); + +let a = matches.get_flag("a"); +``` + +```rust +use uutils_args::{Arguments, Options}; + +#[derive(Arguments)] +enum Arg { + #[arg("-a")] + A +} + +struct Settings { a: bool } + +impl Default for Settings { + fn default() -> Self { + Self { a: false } + } +} + +impl Options for Settings { + fn apply(&mut self, arg: Arg) { + match arg { + Arg::A => self.a = false, + } + } +} + +let a = Settings::default().parse(std::env::args_os()).a; +``` + +### `ArgAction::Count` + +```rust,ignore +let command = Command::new(...) + .arg( + Arg::new("a") + .short('a') + .action(ArgAction::Count) + ); + +let matches = command.get_matches(); + +let a = matches.get_one("a").unwrap(); +``` + +```rust +use uutils_args::{Arguments, Options}; + +#[derive(Arguments)] +enum Arg { + #[arg("-a")] + A +} + +#[derive(Default)] +struct Settings { a: u8 } + +impl Options for Settings { + fn apply(&mut self, arg: Arg) { + match arg { + Arg::A => self.a += 1, + } + } +} + +let a = Settings::default().parse(std::env::args_os()).a; +``` + +### `ArgAction::Set` + +```rust,ignore +let command = Command::new(...) + .arg( + Arg::new("a") + .short('a') + .action(ArgAction::Set) + .value_name("VAL") + ); + +let matches = command.get_matches(); + +let a = matches.get_one("a").unwrap(); +``` + +```rust +use uutils_args::{Arguments, Options}; +use std::ffi::OsString; + +#[derive(Arguments)] +enum Arg { + #[arg("-a VAL")] + A(OsString) +} + +#[derive(Default)] +struct Settings { a: OsString } + +impl Options for Settings { + fn apply(&mut self, arg: Arg) { + match arg { + Arg::A(s) => self.a = s, + } + } +} + +let a = Settings::default().parse(std::env::args_os()).a; +``` + +### `ArgAction::Append` + +```rust,ignore +let command = Command::new(...) + .arg( + Arg::new("a") + .short('a') + .action(ArgAction::Append) + .value_name("VAL") + ); + +let matches = command.get_matches(); + +let a = matches.get_one("a").unwrap(); +``` + +```rust +use uutils_args::{Arguments, Options}; +use std::ffi::OsString; + +#[derive(Arguments)] +enum Arg { + #[arg("-a VAL")] + A(OsString) +} + +#[derive(Default)] +struct Settings { a: Vec } + +impl Options for Settings { + fn apply(&mut self, arg: Arg) { + match arg { + Arg::A(s) => self.a.push(s), + } + } +} + +let a = Settings::default().parse(std::env::args_os()).a; +``` From c8dc4793a0b7432e318d97c528c60e3ee08bb41c Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Thu, 14 Dec 2023 15:32:32 +0100 Subject: [PATCH 05/11] expand design documentation --- docs/design/design.md | 7 + docs/design/iterative.md | 217 ------------------------------ docs/design/problems_with_clap.md | 13 +- 3 files changed, 14 insertions(+), 223 deletions(-) delete mode 100644 docs/design/iterative.md diff --git a/docs/design/design.md b/docs/design/design.md index 3d14cb7..2944283 100644 --- a/docs/design/design.md +++ b/docs/design/design.md @@ -1 +1,8 @@ # Design + +This module contains some documents about the design of this library. In particular, it details the different kinds of arguments that are present in the coreutils and the difficulties that `clap` presents when implementing these arguments. + +## Chapters + +1. [Arguments in the coreutils](design::coreutils) +2. [Problems with `clap`](design::problems) diff --git a/docs/design/iterative.md b/docs/design/iterative.md deleted file mode 100644 index 2fcec1a..0000000 --- a/docs/design/iterative.md +++ /dev/null @@ -1,217 +0,0 @@ -# Library design - -In this document, I explain how this library solves the problems with `clap` and -how it accomplishes the design goals. - -## Basic API - -This library only has a derive API. In most derive-based argument parsers, the -arguments are based on a `struct`, but in this library they are based on `enum` -variants, which then get mapped to a `struct`. The parsing happens in two stages - -1. Arguments get mapped to an `enum` -2. The `enum` variants are matched and update `struct` fields. - -This gives us a separation of concerns: the `enum` determines how the arguments -get parsed and the `struct` determines how they map to the program settings. -This gives us a lot of freedom in defining our mapping from arguments to -settings. - -Here is a simple example comparing `clap` and `uutils_args`. - -> **Note**: There are differences in behaviour between these two. E.g. -> uutils_args allows options to appear multiple times, remembering only the last -> one. - -```rust -// Clap -#[derive(Parser)] -struct Args { - /// Name of the person to greet - #[arg(short, long)] - name: String, - - /// Number of times to greet - #[arg(short, long)] - say_goodbye: bool, -} - -// Uutils args -#[derive(Arguments, Clone)] -enum Arg { - /// Name of the person to greet - #[option("-n NAME", "--name=NAME")] - Name(String), - - /// Number of times to greet - #[option("-g", "--goodbye")] - SayGoodbye -} - -#[derive(Options, Default)] -#[arg_type(Arg)] -struct Settings { - #[set(Arg::Name)] - name: String - - #[map(Arg::SayGoodbye => true)] - goodbye: bool, -} -``` - -> **Note**: `uutils_args` is more explicit than `clap`, you have to explicitly -> state the names of the flags and values. This helps maintainability because it -> is always obvious where an argument is defined. - -As part of the `Options` derive, we get a `Settings::parse` method that returns -a `Settings` from a `OsString` iterator. The implementation of this is defined -by the `set` and `map` attributes. `map` just says: "if we encounter this value -in the iterator set this value", using a match-like syntax (it expands to a -match). And the `#[set(Arg::Name)]` is just short for -`#[map(Arg::Name(name) => name)]`, because that is a commonly appearing pattern. - -Importantly, arguments can appear in the attributes for multiple fields. We -could for instance do this: - -```rust -#[derive(Arguments, Clone)] -enum Arg { - #[option("-a")] - A, - - #[option("--a-and-b")] - B -} - -#[derive(Options, Default)] -#[arg_type(Arg)] -struct Settings { - #[map(Arg::A | Arg::B => true)] - a: bool - - #[map(Arg::B => true)] - b: bool, -} -``` - -## Argument types - -```rust -#[derive(Arguments, Clone)] -#[help("--help")] // help and version must be explicitly defined -#[version("--version")] -enum Arg { - // Note: You can have as many flags as you want for each variable - #[option("-f", "--foo")] - Flag, - - // Note: The value name is required and will be used in `--help` - #[option("-r VALUE", "--required=VALUE")] - OptionWithRequiredValue(String), - - // Note: The value name is again required. - // Note: If no `default` is specified, `Default::default` is used. - #[option("-o[VALUE]", "--optional[=VALUE]", default = "DEFAULT".into())] - OptionWithOptionalValue(String), - - // Note: `-l` will use the default value. - #[option("-l", "--long=VALUE", default = "SHORT VALUE")] - ValueOnlyForLongOption(String), - - // Any combination of required, optional and no arguments is possible. - #[option("-t VAL", "--test[=VAL]", default = "")] - ValueOptionalForLongOption(String), - - // Positional arguments take a range of the number of arguments they - // take. The default is 1..=1, i.e. exactly 1 argument. - #[positional] - SinglePositionalArgument(String), - - #[positional(0..=1)] - OptionalPositionalArgument(String), - - // Range is open on both sides so 0..=MAX - #[positional(..)] - AnyNumberOfPositionalArguments(String), - - // All remaining arguments are collected into a `Vec`. - #[position(last)] - TrailingVarArg(Vec), - - // Same range can still be applied even though there can only ever - // be 1 trailing var arg. - #[position(last, 0..=1)] - OptionalTrailingVarArg(Vec), -} -``` - -## Options struct - -The options struct has just one fundamental attribute: `map`. It works much like -a `match` expression (in fact, that's what it expands to). Furthermore, it's -possible to define defaults on fields. - -```rust -#[derive(Options, Default)] -struct Settings { - // When a Arg::Foo is parsed, set this field to `true`. - // Any expression is possible. - // Any field starts with `Default::default()`. - #[map(Arg::Foo => true)] - foo: bool - - // Arg::BarTrue sets this to true, Arg::BarFalse sets this to false. - // We can have as many arms as we want. For each field, the first - // matching arm is applied and the rest is ignored. - #[map( - Arg::BarTrue => true, - Arg::BarFalse => false, - )] - bar: bool, - - // We can set a default value with the field attribute. - #[map(Arg::Baz => false)] - #[field(default = true)] - baz: bool, - - // We can also define a env var to read from if available, else - // the default value will be used. - #[map(Arg::SomeVar => true)] - #[field(env = "SOME_VAR", default = false)] - some_var: bool, -} -``` - -As a shorthand, there is also a `set` attribute. These fields behave -identically: - -```rust -#[derive(Options, Default)] -struct Settings { - #[map(Arg::Foo(f) => f)] - bar: u64, - - #[set(Arg::Foo)] - baz: u64 -} -``` - -## `FromValue` enums - -We often want to map values to some enum, we can define this mapping by deriving -`FromValue`: - -```rust -#[derive(Default, FromValue)] -enum Color { - #[value("always", "yes", "force")] - Always, - - #[default] - #[value("auto", "tty", "if-tty")] - Auto, - - #[value("never", "no", "none")] - Never, -} -``` diff --git a/docs/design/problems_with_clap.md b/docs/design/problems_with_clap.md index 1650c18..d3c8774 100644 --- a/docs/design/problems_with_clap.md +++ b/docs/design/problems_with_clap.md @@ -8,9 +8,9 @@ inspiration from them. Before I continue, I want to note that these are not (always) general problems with `clap`. They are problems that show up when you want to implement the coreutils with it. The coreutils have some weird behaviour that you won't have -to deal with in a new project. `clap` is still a really good library and you +to deal with in a new project. `clap` is still a great library, and you should probably use it over this library, unless you need compatibility with GNU -utils. +utilities. ## Problem 1: No many-to-many relationship between arguments and settings @@ -18,7 +18,7 @@ This is the biggest issue we have with `clap`. In `clap`, it is assumed that options do not interfere with each other. This means that _partially overriding_ options are really hard to support. `rm` has `--interactive` and `-f`, which mostly just override each other, because they set the interactive mode and -decide whether to print warnings. However, `--interactive=never` does nog change +decide whether to print warnings. However, `--interactive=never` does not change whether warnings are printed. Hence, they cannot override completely, because then these two are **not** identical: @@ -59,9 +59,10 @@ Changing these defaults is sometimes just a single line, but other times it becomes quite verbose. In particular, setting the options to override becomes quite verbose in some cases. -[^1]: There is a setting to set it for all arguments, but it behaves differently -than setting it individually and leads to some troubles, due to the differences -mentioned in the next section. +[^1]: + There is a setting to set it for all arguments, but it behaves differently + than setting it individually and leads to some troubles, due to the differences + mentioned in the next section. ## Problem 4: Subtle differences From 04131a41c8b05aaa5a93fdf60c550f40c0b5e480 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Thu, 14 Dec 2023 15:32:42 +0100 Subject: [PATCH 06/11] document completion generation --- docs/guide/completions.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/guide/completions.md b/docs/guide/completions.md index e4228de..efecf40 100644 --- a/docs/guide/completions.md +++ b/docs/guide/completions.md @@ -1 +1,15 @@ # Completions + +Shell completions and documentation can be generated automatically by this crate. The implementation for this is in the [`uutils-args-complete`] crate. The easiest way of generating completions is via the `parse-is-complete` feature flag. This feature flag hijacks the [`Options::parse`](crate::Options::parse) function to print completions. This means that there is usually no need to write any additional code to generate completions. + +```bash +cargo run --features parse-is-complete -- [shell] +``` + +The `[shell]` value here can be `fish`, `zsh`, `bash`, `powershell`, `elvish` or `nu`. + +> **Note**: Some of these remain unimplemented as of writing. + +Additionally, the values `man` or `md` can be passed to generate man pages and markdown documentation (for `mdbook`). + +If you do not want to hijack the [`Options::parse`](crate::Options::parse) function, you can instead enable the `complete` feature flag. This makes the `Options::complete` function available in addition to the [`Options::parse`](crate::Options::parse) function to generate a `String` with the completion. From 3214c735340978c0f5e5fd81216d9a19e91063f6 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Thu, 14 Dec 2023 15:36:57 +0100 Subject: [PATCH 07/11] move information from index.md to design.md --- docs/design/design.md | 16 ++++++++++++++++ docs/index.md | 24 ------------------------ 2 files changed, 16 insertions(+), 24 deletions(-) delete mode 100644 docs/index.md diff --git a/docs/design/design.md b/docs/design/design.md index 2944283..c79b192 100644 --- a/docs/design/design.md +++ b/docs/design/design.md @@ -2,6 +2,22 @@ This module contains some documents about the design of this library. In particular, it details the different kinds of arguments that are present in the coreutils and the difficulties that `clap` presents when implementing these arguments. +The primary design considerations of this library are: + +- Must support all options in GNU coreutils. +- Must support a many-to-many relationship between options and settings. +- Must have a convenient derive API. +- Must support help strings from file. +- Code must be "greppable" (e.g. search file for `--all` to find the code for + that argument). +- Maintainability is more important than terseness. +- With a bit of luck, it will be smaller and faster than `clap`, because we have + fewer features to support. +- Use outside uutils is possible but not prioritized. Hence, configurability + beyond the coreutils is not necessary. +- Errors must be at least as good as GNU's, but may be different (hopefully + improved). + ## Chapters 1. [Arguments in the coreutils](design::coreutils) diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 22b94bf..0000000 --- a/docs/index.md +++ /dev/null @@ -1,24 +0,0 @@ -# `uutils-args` Design Docs - -This is a series of design documents, explaining the various design goals and -decisions. Before diving in, let's lay out the design goals of this project. - -- Must support all options in GNU coreutils. -- Must support a many-to-many relationship between options and settings. -- Must have a convenient derive API. -- Must support help strings from file. -- Code must be "greppable" (e.g. search file for `--all` to find the code for - that argument). -- Maintainability is more important than terseness. -- With a bit of luck, it will be smaller and faster than `clap`, because we have - fewer features to support. -- Use outside uutils is possible but not prioritized. Hence, configurability - beyond the coreutils is not necessary. -- Errors must be at least as good as GNU's, but may be different (hopefully - improved). - -## Pages - -1. [Arguments in coreutils](arguments_in_coreutils.md) -2. [Problems with `clap` and other parsers](problems_with_clap.md) -3. [Library design](design.md) (TODO once the design settles) From 50cd4b14941568d2a30d15418e03d7597a9e8c82 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Tue, 19 Dec 2023 12:30:45 +0100 Subject: [PATCH 08/11] update docs to new positional arguments --- docs/guide/port.md | 10 ++--- docs/guide/quick.md | 92 ++++++++++++++++++--------------------------- 2 files changed, 41 insertions(+), 61 deletions(-) diff --git a/docs/guide/port.md b/docs/guide/port.md index c99739a..a154a10 100644 --- a/docs/guide/port.md +++ b/docs/guide/port.md @@ -74,7 +74,7 @@ impl Options for Settings { } } -let a = Settings::default().parse(std::env::args_os()).a; +let a = Settings::default().parse(std::env::args_os()).0.a; ``` ### `ArgAction::SetFalse` @@ -117,7 +117,7 @@ impl Options for Settings { } } -let a = Settings::default().parse(std::env::args_os()).a; +let a = Settings::default().parse(std::env::args_os()).0.a; ``` ### `ArgAction::Count` @@ -155,7 +155,7 @@ impl Options for Settings { } } -let a = Settings::default().parse(std::env::args_os()).a; +let a = Settings::default().parse(std::env::args_os()).0.a; ``` ### `ArgAction::Set` @@ -195,7 +195,7 @@ impl Options for Settings { } } -let a = Settings::default().parse(std::env::args_os()).a; +let a = Settings::default().parse(std::env::args_os()).0.a; ``` ### `ArgAction::Append` @@ -235,5 +235,5 @@ impl Options for Settings { } } -let a = Settings::default().parse(std::env::args_os()).a; +let a = Settings::default().parse(std::env::args_os()).0.a; ``` diff --git a/docs/guide/quick.md b/docs/guide/quick.md index 5f5a12d..2a150b9 100644 --- a/docs/guide/quick.md +++ b/docs/guide/quick.md @@ -13,8 +13,11 @@ We can create arguments by annotating a variant of an `enum` deriving [`Argument To represent the program configuration we create a struct called `Settings`, which implements `Options`. When an argument is encountered, we _apply_ it to the `Settings` struct. In this case, we set the `force` field of `Settings` to `true` if `Arg::Force` is parsed. +Any arguments that are not flags are returned as well as part of the tuple returned by `parse`. These do not have special treatment in this library. + ```rust use uutils_args::{Arguments, Options}; +use std::ffi::OsString; #[derive(Arguments)] enum Arg { @@ -35,8 +38,16 @@ impl Options for Settings { } } -assert!(!Settings::default().parse(["test"]).force); -assert!(Settings::default().parse(["test", "-f"]).force); +let (settings, operands) = Settings::default().parse(["test"]); +assert!(!settings.force); +assert_eq!(operands, Vec::::new()); + +let (settings, operands) = Settings::default().parse(["test", "-f"]); +assert!(settings.force); + +let (settings, operands) = Settings::default().parse(["test", "foo"]); +assert!(!settings.force); +assert_eq!(operands, vec![OsString::from("foo")]); ``` ## Two overriding flags @@ -45,6 +56,7 @@ Of course, we can define multiple flags. If these arguments change the same fiel ```rust use uutils_args::{Arguments, Options}; +use std::ffi::OsString; #[derive(Arguments)] enum Arg { @@ -68,9 +80,18 @@ impl Options for Settings { } } -assert!(!Settings::default().parse(["test"]).force); -assert!(Settings::default().parse(["test", "-f"]).force); -assert!(!Settings::default().parse(["test", "-f", "-F"]).force); +let (settings, operands) = Settings::default().parse(["test"]); +assert!(!settings.force); +assert_eq!(operands, Vec::::new()); + +let (settings, operands) = Settings::default().parse(["test", "-f", "some-operand"]); +assert!(settings.force); + +assert_eq!(operands, vec!["some-operand"]); +let (settings, operands) = Settings::default().parse(["test", "-f", "-F", "some-other-operand"]); +assert!(!settings.force); + +assert_eq!(operands, vec!["some-other-operand"]); ``` ## Help strings @@ -120,11 +141,11 @@ enum Arg { # } # # assert_eq!( -# Settings::default().parse(["test"]).name, +# Settings::default().parse(["test"]).0.name, # OsString::new(), # ); # assert_eq!( -# Settings::default().parse(["test", "--name=John"]).name, +# Settings::default().parse(["test", "--name=John"]).0.name, # OsString::from("John"), # ); ``` @@ -157,11 +178,11 @@ enum Arg { # } # # assert_eq!( -# Settings::default().parse(["test", "--name"]).name, +# Settings::default().parse(["test", "--name"]).0.name, # OsString::from("anonymous"), # ); # assert_eq!( -# Settings::default().parse(["test", "--name=John"]).name, +# Settings::default().parse(["test", "--name=John"]).0.name, # OsString::from("John"), # ); ``` @@ -193,9 +214,9 @@ enum Arg { # } # } # -# assert!(!Settings::default().parse(["test"]).force); -# assert!(Settings::default().parse(["test", "-f"]).force); -# assert!(!Settings::default().parse(["test", "-F"]).force); +# assert!(!Settings::default().parse(["test"]).0.force); +# assert!(Settings::default().parse(["test", "-f"]).0.force); +# assert!(!Settings::default().parse(["test", "-F"]).0.force); ``` This is particularly interesting for defining "shortcut" arguments. For example, `ls` takes a `--sort=WORD` argument, that defines how the files should be sorted. But it also has shorthands like `-t`, which is the same as `--sort=time`. All of these can be implemented on one variant: @@ -228,48 +249,7 @@ enum Arg { # } # } # -# assert_eq!(Settings::default().parse(["test"]).sort, String::new()); -# assert_eq!(Settings::default().parse(["test", "--sort=time"]).sort, String::from("time")); -# assert_eq!(Settings::default().parse(["test", "-t"]).sort, String::from("time")); +# assert_eq!(Settings::default().parse(["test"]).0.sort, String::new()); +# assert_eq!(Settings::default().parse(["test", "--sort=time"]).0.sort, String::from("time")); +# assert_eq!(Settings::default().parse(["test", "-t"]).0.sort, String::from("time")); ``` - -## Positional arguments - -Finally, at the end of this whirlwind tour, we get to positional arguments. Here's a simple positional argument: - -```rust -use uutils_args::{Arguments, Options}; -use std::path::PathBuf; - -#[derive(Arguments)] -enum Arg { - #[arg("FILES", ..)] - File(PathBuf) -} - -#[derive(Default, Debug, PartialEq, Eq)] -struct Settings { - files: Vec, -} - -impl Options for Settings { - fn apply(&mut self, arg: Arg) { - match arg { - Arg::File(f) => self.files.push(f), - } - } -} -# -# assert_eq!( -# Settings::default().parse(["test"]).files, -# Vec::::new(), -# ); -# assert_eq!( -# Settings::default().parse(["test", "foo"]).files, -# vec![PathBuf::from("foo")], -# ); -# assert_eq!( -# Settings::default().parse(["test", "foo", "bar"]).files, -# vec![PathBuf::from("foo"), PathBuf::from("bar")], -# ); -``` \ No newline at end of file From 88019ffe4b4257b0f3043aabb2516265384cedda Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Tue, 19 Dec 2023 13:22:39 +0100 Subject: [PATCH 09/11] add docs for `Value` trait and derive --- derive/src/lib.rs | 20 ++++------------ docs/guide/value.md | 56 +++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 35 +++++++++++++++++++++++++++- 3 files changed, 95 insertions(+), 16 deletions(-) diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 5a74dfe..486530c 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -1,6 +1,9 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +//! Derive macros for `uutils_args`. All items here are documented in that +//! crate. + mod argument; mod attributes; mod complete; @@ -18,21 +21,7 @@ use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, Data::Enum, DeriveInput}; -/// Derive `Arguments` -/// -/// ## Argument specifications -/// -/// | specification | kind | value | -/// | -------------- | ---------- | -------- | -/// | `VAL` | positional | n.a. | -/// | `-s` | short | none | -/// | `-s VAL` | short | required | -/// | `-s[VAL]` | short | optional | -/// | `--long` | long | none | -/// | `--long=VAL` | long | required | -/// | `--long[=VAL]` | long | optional | -/// | `long=VAL` | dd | required | -/// +/// Documentation for this can be found in `uutils_args`. #[proc_macro_derive(Arguments, attributes(arg, arguments))] pub fn arguments(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); @@ -125,6 +114,7 @@ pub fn arguments(input: TokenStream) -> TokenStream { TokenStream::from(expanded) } +/// Documentation for this can be found in `uutils_args`. #[proc_macro_derive(Value, attributes(value))] pub fn value(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); diff --git a/docs/guide/value.md b/docs/guide/value.md index 4c689c8..7cbff6e 100644 --- a/docs/guide/value.md +++ b/docs/guide/value.md @@ -1 +1,57 @@ # Value trait + +Any field on the enum implementing [`Arguments`](trait@crate::Arguments) has to implement the [`Value`](trait@crate::Value) trait, which determines how it is derive from the text value. Normally, [`Value`](trait@crate::Value) only requires one method: [`from_value`](crate::Value::from_value), which takes an `&OsStr` and returns a `Result` with either `Self` or some boxed error. + +This trait is implemented for common types, such as integers, [`OsString`](std::ffi::OsString), [`PathBuf`](std::path::PathBuf), [`String`] and [`Option`] where `T` implements `Value`. + +There is also a [`Value` derive macro](derive@crate::Value), which provides parsing string values into an `enum`. The name of each variant (lowercased) with a `#[value]` attribute is parsed automatically. Additionally, if the string is an unambiguous prefix, it is also parsed. For example, if we have the values `"yes"` and `"no"` then `"y"`, `"ye"`, `"yes"` are all valid for `"yes"`, because no other values start with those substrings. + +```rust +use uutils_args::Value; +use std::ffi::OsStr; + +#[derive(Value, Debug, PartialEq, Eq)] +enum YesOrNo { + #[value] + Yes, + #[value] + No, +} + +assert_eq!(YesOrNo::from_value(OsStr::new("yes")).unwrap(), YesOrNo::Yes); +assert_eq!(YesOrNo::from_value(OsStr::new("no")).unwrap(), YesOrNo::No); +assert_eq!(YesOrNo::from_value(OsStr::new("y")).unwrap(), YesOrNo::Yes); +assert_eq!(YesOrNo::from_value(OsStr::new("n")).unwrap(), YesOrNo::No); +assert!(YesOrNo::from_value(OsStr::new("YES")).is_err()); +assert!(YesOrNo::from_value(OsStr::new("NO")).is_err()); +assert!(YesOrNo::from_value(OsStr::new("maybe")).is_err()); +``` + +We can also provide custom names for the variants. This is useful if there are multiple strings that should parse to one variant. + +```rust +use uutils_args::Value; +use std::ffi::OsStr; + +#[derive(Value, Debug, PartialEq, Eq)] +enum Color { + #[value("yes", "always")] + Always, + #[value("auto")] + Auto, + #[value("no", "never")] + Never, +} + +assert_eq!(Color::from_value(&OsStr::new("yes")).unwrap(), Color::Always); +assert_eq!(Color::from_value(&OsStr::new("always")).unwrap(), Color::Always); +assert_eq!(Color::from_value(&OsStr::new("auto")).unwrap(), Color::Auto); +assert_eq!(Color::from_value(&OsStr::new("no")).unwrap(), Color::Never); +assert_eq!(Color::from_value(&OsStr::new("never")).unwrap(), Color::Never); + +// The prefixes here are interesting: +// - "a" is ambiguous because it is a prefix of "auto" and "always" +// - "n" is not ambiguous because "no" and "never" map to the same variant +assert!(Color::from_value(&OsStr::new("a")).is_err()); +assert_eq!(Color::from_value(&OsStr::new("n")).unwrap(), Color::Never); +``` diff --git a/src/lib.rs b/src/lib.rs index 87716a1..a7afc2d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,7 +12,40 @@ mod value; pub mod docs; pub use lexopt; -pub use uutils_args_derive::*; + +// The documentation for the derive macros is written here instead of in +// `uutils_args_derive`, because we need to be able to link to items and the +// documentation in this crate. + +/// Derive macro for [`Value`](trait@crate::Value) +/// +/// [See also the chapter on this trait in the guide](crate::docs::guide::value) +/// +/// This macro only works on `enums` and will error at compile time when it is +/// used on a `struct`. +pub use uutils_args_derive::Value; + +/// Derive macro for [`Arguments`](trait@crate::Arguments) +/// +/// [See also the chapter on this trait in the guide](crate::docs::guide::quick) +/// +/// This macro only works on `enums` and will error at compile time when it is +/// used on a `struct`. +/// +/// /// ## Argument specifications +/// +/// | specification | kind | value | +/// | -------------- | ---------- | -------- | +/// | `VAL` | positional | n.a. | +/// | `-s` | short | none | +/// | `-s VAL` | short | required | +/// | `-s[VAL]` | short | optional | +/// | `--long` | long | none | +/// | `--long=VAL` | long | required | +/// | `--long[=VAL]` | long | optional | +/// | `long=VAL` | dd | required | +/// +pub use uutils_args_derive::Arguments; pub use error::{Error, ErrorKind}; pub use value::{Value, ValueError, ValueResult}; From f01d94d8dae8bbba222f009f7609b4aa0d437fce Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Tue, 19 Dec 2023 14:39:22 +0100 Subject: [PATCH 10/11] add navigation links to docs --- docs/guide/completions.md | 35 +++++++++++++++++++++++++++++++++++ docs/guide/port.md | 35 +++++++++++++++++++++++++++++++++++ docs/guide/quick.md | 36 ++++++++++++++++++++++++++++++++++++ docs/guide/value.md | 35 +++++++++++++++++++++++++++++++++++ src/docs.rs | 26 ++++++++++++++++++-------- src/lib.rs | 2 +- 6 files changed, 160 insertions(+), 9 deletions(-) diff --git a/docs/guide/completions.md b/docs/guide/completions.md index efecf40..f325b91 100644 --- a/docs/guide/completions.md +++ b/docs/guide/completions.md @@ -1,3 +1,30 @@ + +
+ +[Previous](previous) +[Up](super) +[Next]() + +
+ # Completions Shell completions and documentation can be generated automatically by this crate. The implementation for this is in the [`uutils-args-complete`] crate. The easiest way of generating completions is via the `parse-is-complete` feature flag. This feature flag hijacks the [`Options::parse`](crate::Options::parse) function to print completions. This means that there is usually no need to write any additional code to generate completions. @@ -13,3 +40,11 @@ The `[shell]` value here can be `fish`, `zsh`, `bash`, `powershell`, `elvish` or Additionally, the values `man` or `md` can be passed to generate man pages and markdown documentation (for `mdbook`). If you do not want to hijack the [`Options::parse`](crate::Options::parse) function, you can instead enable the `complete` feature flag. This makes the `Options::complete` function available in addition to the [`Options::parse`](crate::Options::parse) function to generate a `String` with the completion. + +
+ +[Previous](previous) +[Up](super) +[Next]() + +
\ No newline at end of file diff --git a/docs/guide/port.md b/docs/guide/port.md index a154a10..2cc2981 100644 --- a/docs/guide/port.md +++ b/docs/guide/port.md @@ -1,3 +1,30 @@ + +
+ +[Previous](previous) +[Up](super) +[Next](next) + +
+ # Porting from Clap This chapter contains information about how common patterns in `clap` parsers can be ported to `uutils-args`. @@ -237,3 +264,11 @@ impl Options for Settings { let a = Settings::default().parse(std::env::args_os()).0.a; ``` + +
+ +[Previous](previous) +[Up](super) +[Next](next) + +
\ No newline at end of file diff --git a/docs/guide/quick.md b/docs/guide/quick.md index 2a150b9..6b928ea 100644 --- a/docs/guide/quick.md +++ b/docs/guide/quick.md @@ -1,3 +1,31 @@ + +
+ +[Previous]() +[Up](super) +[Next](next) + +
+ # Quick Start A parser consists of two parts: @@ -253,3 +281,11 @@ enum Arg { # assert_eq!(Settings::default().parse(["test", "--sort=time"]).0.sort, String::from("time")); # assert_eq!(Settings::default().parse(["test", "-t"]).0.sort, String::from("time")); ``` + +
+ +[Previous]() +[Up](super) +[Next](next) + +
\ No newline at end of file diff --git a/docs/guide/value.md b/docs/guide/value.md index 7cbff6e..443a960 100644 --- a/docs/guide/value.md +++ b/docs/guide/value.md @@ -1,3 +1,30 @@ + +
+ +[Previous](previous) +[Up](super) +[Next](next) + +
+ # Value trait Any field on the enum implementing [`Arguments`](trait@crate::Arguments) has to implement the [`Value`](trait@crate::Value) trait, which determines how it is derive from the text value. Normally, [`Value`](trait@crate::Value) only requires one method: [`from_value`](crate::Value::from_value), which takes an `&OsStr` and returns a `Result` with either `Self` or some boxed error. @@ -55,3 +82,11 @@ assert_eq!(Color::from_value(&OsStr::new("never")).unwrap(), Color::Never); assert!(Color::from_value(&OsStr::new("a")).is_err()); assert_eq!(Color::from_value(&OsStr::new("n")).unwrap(), Color::Never); ``` + +
+ +[Previous](previous) +[Up](super) +[Next](next) + +
\ No newline at end of file diff --git a/src/docs.rs b/src/docs.rs index 3168ade..045ef04 100644 --- a/src/docs.rs +++ b/src/docs.rs @@ -5,14 +5,24 @@ #[doc = include_str!("../docs/guide/guide.md")] pub mod guide { - #[doc = include_str!("../docs/guide/quick.md")] - pub mod quick {} - #[doc = include_str!("../docs/guide/port.md")] - pub mod port {} - #[doc = include_str!("../docs/guide/completions.md")] - pub mod completions {} - #[doc = include_str!("../docs/guide/value.md")] - pub mod value {} + pub mod quick { + #![doc = include_str!("../docs/guide/quick.md")] + pub use super::port as next; + } + pub mod port { + #![doc = include_str!("../docs/guide/port.md")] + pub use super::quick as previous; + pub use super::value as next; + } + pub mod value { + #![doc = include_str!("../docs/guide/value.md")] + pub use super::completions as next; + pub use super::port as previous; + } + pub mod completions { + #![doc = include_str!("../docs/guide/completions.md")] + pub use super::value as previous; + } } #[doc = include_str!("../docs/design/design.md")] diff --git a/src/lib.rs b/src/lib.rs index a7afc2d..9d0a4e8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,7 +31,7 @@ pub use uutils_args_derive::Value; /// /// This macro only works on `enums` and will error at compile time when it is /// used on a `struct`. -/// +/// /// /// ## Argument specifications /// /// | specification | kind | value | From 49ede1997c707f15b90021eaf4103ab0782bc030 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Tue, 19 Dec 2023 14:47:09 +0100 Subject: [PATCH 11/11] make link to guide big --- src/lib.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 9d0a4e8..8e76960 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,12 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -//! [Click here to check out the guide-level documentation](docs::guide) +//!
+//! +//! [Click here for the guide](docs::guide) +//! +//!
+//! #![doc = include_str!("../README.md")] mod error;