diff --git a/derive/src/field.rs b/derive/src/field.rs index 1282bf1..6ec010e 100644 --- a/derive/src/field.rs +++ b/derive/src/field.rs @@ -21,10 +21,9 @@ pub(crate) fn parse_field(field: &Field) -> FieldData { if let Some(env_var) = field_attr.env { default_value = quote!( - match ::std::env::var_os(#env_var) { - Some(x) => ::uutils_args::FromValue::from_value("", x)?, - None => #default_value - } + ::std::env::var_os(#env_var) + .and_then(|v| ::uutils_args::FromValue::from_value("", v).ok()) + .unwrap_or(#default_value) ) } diff --git a/derive/src/lib.rs b/derive/src/lib.rs index c5b3b25..30dc934 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -50,10 +50,10 @@ pub fn initial(input: TokenStream) -> TokenStream { let expanded = quote!( impl #impl_generics Initial for #name #ty_generics #where_clause { - fn initial() -> Result { - Ok(Self { + fn initial() -> Self { + Self { #(#defaults),* - }) + } } } ); diff --git a/src/error.rs b/src/error.rs index 3bbb2ed..63d9ab2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -4,32 +4,49 @@ use std::{ fmt::{Debug, Display}, }; +/// Errors that can occur while parsing arguments. pub enum Error { - MissingValue { - option: Option, - }, + /// There was an option that required an option, but none was given. + MissingValue { option: Option }, + + /// Some positional arguments were not given. MissingPositionalArguments(Vec), + + /// An unrecognized option was passed. UnexpectedOption(String), + + /// No more positional arguments were expected, but one was given anyway. UnexpectedArgument(OsString), - UnexpectedValue { - option: String, - value: OsString, - }, + + /// A value was passed to an option that didn't expect a value. + UnexpectedValue { option: String, value: OsString }, + + /// Parsing of a value failed. ParsingFailed { option: String, value: String, error: Box, }, + + /// An abbreviated long option was given that could match multiple + /// long options. AmbiguousOption { option: String, candidates: Vec, }, + + /// An abbreviated value was given that could match multiple values. AmbiguousValue { option: String, value: String, candidates: Vec, }, + + /// The value was required to be valid UTF-8, but it wasn't. NonUnicodeValue(OsString), + + /// Some custom error, probably returned by a + /// [`FromValue`](crate::FromValue) implementation. Custom(Box), } diff --git a/src/lib.rs b/src/lib.rs index ad21b11..76ca21c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,30 @@ //! Argument parsing for the uutils coreutils project //! -//! This crate provides the argument parsing for the uutils coreutils -//! project. It is designed to be flexible, while providing default +//! 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. +//! - (Limited) markdown support in help text. +//! - 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 @@ -15,8 +36,8 @@ //! //! For more information on these traits, see their respective documentation: //! -//! - [`Arguments`] -//! - [`Options`] +//! - [`Arguments`] +//! - [`Options`] //! //! Below is a minimal example of a full CLI application using this library. //! @@ -25,7 +46,7 @@ //! //! #[derive(Arguments)] //! enum Arg { -//! // The docstrings below will be part of the `--help` text +//! // 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")] @@ -103,6 +124,21 @@ //! - [`FromValue`] allows for easy parsing from `OsStr` to any type //! implementing [`FromValue`]. This crate also provides a derive macro for //! this trait. +//! +//! # Examples +//! +//! The following files contain examples of commands defined with +//! `uutils_args`: +//! +//! - [hello world](https://github.com/tertsdiepraam/uutils-args/blob/main/examples/hello_world.rs) +//! - [arch](https://github.com/tertsdiepraam/uutils-args/blob/main/tests/coreutils/arch.rs) +//! - [b2sum](https://github.com/tertsdiepraam/uutils-args/blob/main/tests/coreutils/b2sum.rs) +//! - [base32](https://github.com/tertsdiepraam/uutils-args/blob/main/tests/coreutils/base32.rs) +//! - [basename](https://github.com/tertsdiepraam/uutils-args/blob/main/tests/coreutils/basename.rs) +//! - [cat](https://github.com/tertsdiepraam/uutils-args/blob/main/tests/coreutils/cat.rs) +//! - [echo](https://github.com/tertsdiepraam/uutils-args/blob/main/tests/coreutils/echo.rs) +//! - [ls](https://github.com/tertsdiepraam/uutils-args/blob/main/tests/coreutils/ls.rs) +//! - [mktemp](https://github.com/tertsdiepraam/uutils-args/blob/main/tests/coreutils/mktemp.rs) mod error; pub use derive::*; @@ -115,6 +151,8 @@ use std::num::ParseIntError; use std::path::PathBuf; use std::{ffi::OsString, marker::PhantomData}; +/// A wrapper around a type implementing [`Arguments`] that adds `Help` +/// and `Version` variants. #[derive(Clone)] pub enum Argument { Help, @@ -132,9 +170,20 @@ fn exit_if_err(res: Result, exit_code: i32) -> T { } } +/// Defines how the arguments are parsed. +/// +/// If a type `T` implements this trait, we can construct an `ArgumentIter`, +/// meaning that we can parse the individual arguments to `T`.\ +/// +/// Usually, this trait will be implemented via the +/// [derive macro](derive::Arguments) and does not need to be implemented +/// manually. 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, @@ -143,17 +192,34 @@ pub trait Arguments: Sized { 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>; + /// Get the help string for this command. + /// + /// The `bin_name` specifies the name that executable was called with. 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, @@ -162,6 +228,10 @@ pub trait Arguments: Sized { 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> where I: IntoIterator + 'static, @@ -173,6 +243,11 @@ pub trait Arguments: Sized { } } +/// An iterator over arguments. +/// +/// Can be constructed by calling [`Arguments::parse`]. Usually, this method +/// won't be used directly, but is used internally in [`Options::parse`] and +/// [`Options::try_parse`]. pub struct ArgumentIter { parser: lexopt::Parser, pub positional_idx: usize, @@ -219,15 +294,41 @@ impl ArgumentIter { } } +/// An alternative for the [`Default`](std::default::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`](std::default::Default) trait. pub trait Initial: Sized { - fn initial() -> Result; + /// 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: +/// - the [`Arg`](Options::Arg) type, which defines the type to use for +/// argument parsing, +/// - 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 { type Arg: Arguments; + /// Apply a single argument to the options. fn apply(&mut self, arg: Self::Arg); + /// Parse an iterator of arguments into fn parse(args: I) -> Self where I: IntoIterator + 'static, @@ -241,7 +342,7 @@ pub trait Options: Sized + Initial { I: IntoIterator + 'static, I::Item: Into, { - let mut _self = Self::initial()?; + let mut _self = Self::initial(); let mut iter = Self::Arg::parse(args); while let Some(arg) = iter.next_arg()? { _self.apply(arg); @@ -251,6 +352,7 @@ pub trait Options: Sized + Initial { } } +/// Defines how a type should be parsed from an argument. pub trait FromValue: Sized { fn from_value(option: &str, value: OsString) -> Result; }