Skip to content

fix(lexarg): Iterate on the design #81

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 35 commits into from
Mar 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
cd0aa89
docs(lexarg): Clean up the docs
epage Mar 5, 2025
dcf8339
fix(lexarg)!: Clarify Parser::next works with `Arg`
epage Mar 5, 2025
17a3c2c
refactor(lexarg): Clarify next_value isn't general
epage Mar 5, 2025
b9222d5
refactor(lexarg): Clarify how flag_value works
epage Mar 5, 2025
6767504
fix(lexarg)!: Clarify Parser::flag_value advances
epage Mar 5, 2025
5838027
refactor(lexarg): Clarify behavior with asserts
epage Mar 5, 2025
92dd93b
feat(lexarg): Add Parser::next_raw
epage Mar 5, 2025
985214a
fix(lexarg)!: Remove Parser::bin in favor of Parser::next_raw
epage Mar 5, 2025
9dca1eb
fix(lexarg): Allow '-' as a short
epage Mar 6, 2025
46af6ea
fix(lexarg)!: Always allow = as a short flag
epage Mar 6, 2025
e25397d
fix(lexarg): Ensure Escape is reported after an option
epage Mar 6, 2025
2fe9162
docs(design): Clarify another lexarg design decision
epage Mar 6, 2025
57d10d1
refactor(lexarg): Remove extraneous state change
epage Mar 6, 2025
43731fb
test(lexarg): Show bug with attached values
epage Mar 6, 2025
2f08cf5
fix(lexarg): Don't skip ahead to = sign
epage Mar 6, 2025
b3866e4
refactor(lexarg); Ensure next_attached_value is standalone
epage Mar 6, 2025
e1fa243
feat(lexarg): Expose Parser::next_attached_value
epage Mar 7, 2025
a0c0427
feat(lexarg): Expose Parser::remaining_raw
epage Mar 7, 2025
6ab1334
refactor(lexarg): Clarify the error being specialized on index
epage Mar 24, 2025
30d5757
fix(lexarg)!: Switch to `Short(str)`
epage Mar 24, 2025
a2740be
fix(lexarg)!: Switch to `Escape(str)`
epage Mar 24, 2025
aba8d47
feat(lexarg): Add Parser::peek_raw
epage Mar 25, 2025
3646515
feat(lexarg): Make Arg copyable
epage Mar 25, 2025
4dd2845
docs(lexarg): Remove bad example
epage Mar 25, 2025
a59ff1d
docs(lexarg): Clarify docs intro
epage Mar 25, 2025
55096dc
refactor(lexarg): Prefer Self over naming the type
epage Mar 25, 2025
8fde3b2
fix(lexarg)!: Pull out an ErrorContext builder
epage Mar 25, 2025
0026ba8
feat(lexarg): Allow reporting the argument the operation failed within
epage Mar 25, 2025
7442bd5
docs(lexarg): Split up operation in example
epage Mar 25, 2025
19a1bdb
feat(lexarg): Allow reporting the argument that failed
epage Mar 25, 2025
37dfada
docs(lexarg): Fix a bad value in an example
epage Mar 25, 2025
313d30a
docs(lexarg): Pull out doc examples into targets
epage Mar 25, 2025
e40c077
feat(lexarg): Allow using Result in main
epage Mar 25, 2025
b6b8b49
docs(lexarg): Include short help
epage Mar 25, 2025
6eb05e8
docs(lexarg): Add more design justification
epage Mar 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,5 +132,20 @@ this makes delegating to plugins in a cooperative way more challenging.

In reviewing lexopt's API:
- Error handling is included in the API in a way that might make evolution difficult
- Escapes aren't explicitly communicated which makes communal parsing more difficult
- lexopt builds in specific option-value semantics

TODO: there were other points that felt off to me about lexopt's API wrt API stability but I do not recall what they are
And in general we will be putting the parser in the libtest-next's API and it will be a fundamental point of extension.
Having complete control helps ensure the full experience is cohesive.

### Decision: `Short(&str)`

`lexopt` and `clap` / `clap_lex` treat shorts as a `char` which gives a level of type safety to parsing.
However, with a minimal API, providing `&str` provides span information "for free".

If someone were to make an API for pluggable lexers,
support for multi-character shorts is something people may want to opt-in to (it has been requested of clap).

Performance isn't the top priority, so remoing `&str` -> `char` conversions isn't necessarily viewed as a benefit.
This also makes `match` need to work off of `&str` instead of `char`.
Unsure which of those would be slower and how the different characteristics match up.
2 changes: 1 addition & 1 deletion crates/lexarg-error/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ pre-release-replacements = [
default = []

[dependencies]
lexarg = { "version" = "0.1.0", path = "../lexarg" }

[dev-dependencies]
lexarg = { "version" = "0.1.0", path = "../lexarg" }

[lints]
workspace = true
80 changes: 80 additions & 0 deletions crates/lexarg-error/examples/hello-error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use lexarg_error::ErrorContext;
use lexarg_error::Result;

struct Args {
thing: String,
number: u32,
shout: bool,
}

fn parse_args() -> Result<Args> {
#![allow(clippy::enum_glob_use)]
use lexarg::Arg::*;

let mut thing = None;
let mut number = 1;
let mut shout = false;
let raw = std::env::args_os().collect::<Vec<_>>();
let mut parser = lexarg::Parser::new(&raw);
let bin_name = parser
.next_raw()
.expect("nothing parsed yet so no attached lingering")
.expect("always at least one");
while let Some(arg) = parser.next_arg() {
match arg {
Short("n") | Long("number") => {
let value = parser
.next_flag_value()
.ok_or_else(|| ErrorContext::msg("missing required value").within(arg))?;
number = value
.to_str()
.ok_or_else(|| {
ErrorContext::msg("invalid number")
.unexpected(Value(value))
.within(arg)
})?
.parse()
.map_err(|e| ErrorContext::msg(e).unexpected(Value(value)).within(arg))?;
}
Long("shout") => {
shout = true;
}
Value(val) if thing.is_none() => {
thing = Some(
val.to_str()
.ok_or_else(|| ErrorContext::msg("invalid string").unexpected(arg))?,
);
}
Short("h") | Long("help") => {
println!("Usage: hello [-n|--number=NUM] [--shout] THING");
std::process::exit(0);
}
_ => {
return Err(ErrorContext::msg("unexpected argument")
.unexpected(arg)
.within(Value(bin_name))
.into());
}
}
}

Ok(Args {
thing: thing
.ok_or_else(|| ErrorContext::msg("missing argument THING").within(Value(bin_name)))?
.to_owned(),
number,
shout,
})
}

fn main() -> Result<()> {
let args = parse_args()?;
let mut message = format!("Hello {}", args.thing);
if args.shout {
message = message.to_uppercase();
}
for _ in 0..args.number {
println!("{message}");
}
Ok(())
}
202 changes: 90 additions & 112 deletions crates/lexarg-error/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,70 +1,12 @@
//! Argument error type for use with lexarg
//! Error type for use with lexarg
//!
//! Inspired by [lexopt](https://crates.io/crates/lexopt), `lexarg` simplifies the formula down
//! further so it can be used for CLI plugin systems.
//!
//! ## Example
//! ```no_run
//! use lexarg_error::Error;
//! use lexarg_error::Result;
//!
//! struct Args {
//! thing: String,
//! number: u32,
//! shout: bool,
//! }
//!
//! fn parse_args() -> Result<Args> {
//! use lexarg::Arg::*;
//!
//! let mut thing = None;
//! let mut number = 1;
//! let mut shout = false;
//! let mut raw = std::env::args_os().collect::<Vec<_>>();
//! let mut parser = lexarg::Parser::new(&raw);
//! parser.bin();
//! while let Some(arg) = parser.next() {
//! match arg {
//! Short('n') | Long("number") => {
//! number = parser
//! .flag_value().ok_or_else(|| Error::msg("`--number` requires a value"))?
//! .to_str().ok_or_else(|| Error::msg("invalid number"))?
//! .parse().map_err(|e| Error::msg(e))?;
//! }
//! Long("shout") => {
//! shout = true;
//! }
//! Value(val) if thing.is_none() => {
//! thing = Some(val.to_str().ok_or_else(|| Error::msg("invalid number"))?);
//! }
//! Long("help") => {
//! println!("Usage: hello [-n|--number=NUM] [--shout] THING");
//! std::process::exit(0);
//! }
//! _ => {
//! return Err(Error::msg("unexpected argument"));
//! }
//! }
//! }
//!
//! Ok(Args {
//! thing: thing.ok_or_else(|| Error::msg("missing argument THING"))?.to_owned(),
//! number,
//! shout,
//! })
//! }
//!
//! fn main() -> Result<()> {
//! let args = parse_args()?;
//! let mut message = format!("Hello {}", args.thing);
//! if args.shout {
//! message = message.to_uppercase();
//! }
//! for _ in 0..args.number {
//! println!("{}", message);
//! }
//! Ok(())
//! }
//! ```no_run
#![doc = include_str!("../examples/hello-error.rs")]
//! ```

#![cfg_attr(docsrs, feature(doc_auto_cfg))]
Expand All @@ -77,55 +19,10 @@
#[cfg(doctest)]
pub struct ReadmeDoctests;

/// `Result<T, Error>`
///
/// `lexarg_error::Result` may be used with one *or* two type parameters.
///
/// ```rust
/// use lexarg_error::Result;
///
/// # const IGNORE: &str = stringify! {
/// fn demo1() -> Result<T> {...}
/// // ^ equivalent to std::result::Result<T, lexarg_error::Error>
///
/// fn demo2() -> Result<T, OtherError> {...}
/// // ^ equivalent to std::result::Result<T, OtherError>
/// # };
/// ```
///
/// # Example
///
/// ```
/// # pub trait Deserialize {}
/// #
/// # mod serde_json {
/// # use super::Deserialize;
/// # use std::io;
/// #
/// # pub fn from_str<T: Deserialize>(json: &str) -> io::Result<T> {
/// # unimplemented!()
/// # }
/// # }
/// #
/// # #[derive(Debug)]
/// # struct ClusterMap;
/// #
/// # impl Deserialize for ClusterMap {}
/// #
/// use lexarg_error::Result;
///
/// fn main() -> Result<()> {
/// # return Ok(());
/// let config = std::fs::read_to_string("cluster.json")?;
/// let map: ClusterMap = serde_json::from_str(&config)?;
/// println!("cluster info: {:#?}", map);
/// Ok(())
/// }
/// ```
/// `Result` that defaults to [`Error`]
pub type Result<T, E = Error> = std::result::Result<T, E>;

/// Argument error type for use with lexarg
#[derive(Debug)]
pub struct Error {
msg: String,
}
Expand All @@ -137,24 +34,105 @@ impl Error {
where
M: std::fmt::Display,
{
Error {
Self {
msg: message.to_string(),
}
}
}

impl<E> From<E> for Error
impl From<ErrorContext<'_>> for Error {
#[cold]
fn from(error: ErrorContext<'_>) -> Self {
Self::msg(error.to_string())
}
}

impl std::fmt::Debug for Error {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.msg.fmt(formatter)
}
}

impl std::fmt::Display for Error {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.msg.fmt(formatter)
}
}

/// Collect context for creating an [`Error`]
#[derive(Debug)]
pub struct ErrorContext<'a> {
msg: String,
within: Option<lexarg::Arg<'a>>,
unexpected: Option<lexarg::Arg<'a>>,
}

impl<'a> ErrorContext<'a> {
/// Create a new error object from a printable error message.
#[cold]
pub fn msg<M>(message: M) -> Self
where
M: std::fmt::Display,
{
Self {
msg: message.to_string(),
within: None,
unexpected: None,
}
}

/// [`Arg`][lexarg::Arg] the error occurred within
#[cold]
pub fn within(mut self, within: lexarg::Arg<'a>) -> Self {
self.within = Some(within);
self
}

/// The failing [`Arg`][lexarg::Arg]
#[cold]
pub fn unexpected(mut self, unexpected: lexarg::Arg<'a>) -> Self {
self.unexpected = Some(unexpected);
self
}
}

impl<E> From<E> for ErrorContext<'_>
where
E: std::error::Error + Send + Sync + 'static,
{
#[cold]
fn from(error: E) -> Self {
Error::msg(error)
Self::msg(error)
}
}

impl std::fmt::Display for Error {
impl std::fmt::Display for ErrorContext<'_> {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.msg.fmt(formatter)
self.msg.fmt(formatter)?;
if let Some(unexpected) = &self.unexpected {
write!(formatter, ", found `")?;
match unexpected {
lexarg::Arg::Short(short) => write!(formatter, "-{short}")?,
lexarg::Arg::Long(long) => write!(formatter, "--{long}")?,
lexarg::Arg::Escape(value) => write!(formatter, "{value}")?,
lexarg::Arg::Value(value) | lexarg::Arg::Unexpected(value) => {
write!(formatter, "{}", value.to_string_lossy())?;
}
}
write!(formatter, "`")?;
}
if let Some(within) = &self.within {
write!(formatter, " when parsing `")?;
match within {
lexarg::Arg::Short(short) => write!(formatter, "-{short}")?,
lexarg::Arg::Long(long) => write!(formatter, "--{long}")?,
lexarg::Arg::Escape(value) => write!(formatter, "{value}")?,
lexarg::Arg::Value(value) | lexarg::Arg::Unexpected(value) => {
write!(formatter, "{}", value.to_string_lossy())?;
}
}
write!(formatter, "`")?;
}
Ok(())
}
}
Loading
Loading