Skip to content

Commit fce1950

Browse files
authored
Merge pull request #81 from epage/lex
fix(lexarg): Iterate on the design
2 parents 7be3047 + 6eb05e8 commit fce1950

8 files changed

Lines changed: 534 additions & 392 deletions

File tree

DESIGN.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,5 +132,20 @@ this makes delegating to plugins in a cooperative way more challenging.
132132

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

136-
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
138+
And in general we will be putting the parser in the libtest-next's API and it will be a fundamental point of extension.
139+
Having complete control helps ensure the full experience is cohesive.
140+
141+
### Decision: `Short(&str)`
142+
143+
`lexopt` and `clap` / `clap_lex` treat shorts as a `char` which gives a level of type safety to parsing.
144+
However, with a minimal API, providing `&str` provides span information "for free".
145+
146+
If someone were to make an API for pluggable lexers,
147+
support for multi-character shorts is something people may want to opt-in to (it has been requested of clap).
148+
149+
Performance isn't the top priority, so remoing `&str` -> `char` conversions isn't necessarily viewed as a benefit.
150+
This also makes `match` need to work off of `&str` instead of `char`.
151+
Unsure which of those would be slower and how the different characteristics match up.

crates/lexarg-error/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ pre-release-replacements = [
2727
default = []
2828

2929
[dependencies]
30+
lexarg = { "version" = "0.1.0", path = "../lexarg" }
3031

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

3434
[lints]
3535
workspace = true
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
use lexarg_error::ErrorContext;
2+
use lexarg_error::Result;
3+
4+
struct Args {
5+
thing: String,
6+
number: u32,
7+
shout: bool,
8+
}
9+
10+
fn parse_args() -> Result<Args> {
11+
#![allow(clippy::enum_glob_use)]
12+
use lexarg::Arg::*;
13+
14+
let mut thing = None;
15+
let mut number = 1;
16+
let mut shout = false;
17+
let raw = std::env::args_os().collect::<Vec<_>>();
18+
let mut parser = lexarg::Parser::new(&raw);
19+
let bin_name = parser
20+
.next_raw()
21+
.expect("nothing parsed yet so no attached lingering")
22+
.expect("always at least one");
23+
while let Some(arg) = parser.next_arg() {
24+
match arg {
25+
Short("n") | Long("number") => {
26+
let value = parser
27+
.next_flag_value()
28+
.ok_or_else(|| ErrorContext::msg("missing required value").within(arg))?;
29+
number = value
30+
.to_str()
31+
.ok_or_else(|| {
32+
ErrorContext::msg("invalid number")
33+
.unexpected(Value(value))
34+
.within(arg)
35+
})?
36+
.parse()
37+
.map_err(|e| ErrorContext::msg(e).unexpected(Value(value)).within(arg))?;
38+
}
39+
Long("shout") => {
40+
shout = true;
41+
}
42+
Value(val) if thing.is_none() => {
43+
thing = Some(
44+
val.to_str()
45+
.ok_or_else(|| ErrorContext::msg("invalid string").unexpected(arg))?,
46+
);
47+
}
48+
Short("h") | Long("help") => {
49+
println!("Usage: hello [-n|--number=NUM] [--shout] THING");
50+
std::process::exit(0);
51+
}
52+
_ => {
53+
return Err(ErrorContext::msg("unexpected argument")
54+
.unexpected(arg)
55+
.within(Value(bin_name))
56+
.into());
57+
}
58+
}
59+
}
60+
61+
Ok(Args {
62+
thing: thing
63+
.ok_or_else(|| ErrorContext::msg("missing argument THING").within(Value(bin_name)))?
64+
.to_owned(),
65+
number,
66+
shout,
67+
})
68+
}
69+
70+
fn main() -> Result<()> {
71+
let args = parse_args()?;
72+
let mut message = format!("Hello {}", args.thing);
73+
if args.shout {
74+
message = message.to_uppercase();
75+
}
76+
for _ in 0..args.number {
77+
println!("{message}");
78+
}
79+
Ok(())
80+
}

crates/lexarg-error/src/lib.rs

Lines changed: 90 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,12 @@
1-
//! Argument error type for use with lexarg
1+
//! Error type for use with lexarg
22
//!
33
//! Inspired by [lexopt](https://crates.io/crates/lexopt), `lexarg` simplifies the formula down
44
//! further so it can be used for CLI plugin systems.
55
//!
66
//! ## Example
7-
//! ```no_run
8-
//! use lexarg_error::Error;
9-
//! use lexarg_error::Result;
10-
//!
11-
//! struct Args {
12-
//! thing: String,
13-
//! number: u32,
14-
//! shout: bool,
15-
//! }
16-
//!
17-
//! fn parse_args() -> Result<Args> {
18-
//! use lexarg::Arg::*;
197
//!
20-
//! let mut thing = None;
21-
//! let mut number = 1;
22-
//! let mut shout = false;
23-
//! let mut raw = std::env::args_os().collect::<Vec<_>>();
24-
//! let mut parser = lexarg::Parser::new(&raw);
25-
//! parser.bin();
26-
//! while let Some(arg) = parser.next() {
27-
//! match arg {
28-
//! Short('n') | Long("number") => {
29-
//! number = parser
30-
//! .flag_value().ok_or_else(|| Error::msg("`--number` requires a value"))?
31-
//! .to_str().ok_or_else(|| Error::msg("invalid number"))?
32-
//! .parse().map_err(|e| Error::msg(e))?;
33-
//! }
34-
//! Long("shout") => {
35-
//! shout = true;
36-
//! }
37-
//! Value(val) if thing.is_none() => {
38-
//! thing = Some(val.to_str().ok_or_else(|| Error::msg("invalid number"))?);
39-
//! }
40-
//! Long("help") => {
41-
//! println!("Usage: hello [-n|--number=NUM] [--shout] THING");
42-
//! std::process::exit(0);
43-
//! }
44-
//! _ => {
45-
//! return Err(Error::msg("unexpected argument"));
46-
//! }
47-
//! }
48-
//! }
49-
//!
50-
//! Ok(Args {
51-
//! thing: thing.ok_or_else(|| Error::msg("missing argument THING"))?.to_owned(),
52-
//! number,
53-
//! shout,
54-
//! })
55-
//! }
56-
//!
57-
//! fn main() -> Result<()> {
58-
//! let args = parse_args()?;
59-
//! let mut message = format!("Hello {}", args.thing);
60-
//! if args.shout {
61-
//! message = message.to_uppercase();
62-
//! }
63-
//! for _ in 0..args.number {
64-
//! println!("{}", message);
65-
//! }
66-
//! Ok(())
67-
//! }
8+
//! ```no_run
9+
#![doc = include_str!("../examples/hello-error.rs")]
6810
//! ```
6911
7012
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
@@ -77,55 +19,10 @@
7719
#[cfg(doctest)]
7820
pub struct ReadmeDoctests;
7921

80-
/// `Result<T, Error>`
81-
///
82-
/// `lexarg_error::Result` may be used with one *or* two type parameters.
83-
///
84-
/// ```rust
85-
/// use lexarg_error::Result;
86-
///
87-
/// # const IGNORE: &str = stringify! {
88-
/// fn demo1() -> Result<T> {...}
89-
/// // ^ equivalent to std::result::Result<T, lexarg_error::Error>
90-
///
91-
/// fn demo2() -> Result<T, OtherError> {...}
92-
/// // ^ equivalent to std::result::Result<T, OtherError>
93-
/// # };
94-
/// ```
95-
///
96-
/// # Example
97-
///
98-
/// ```
99-
/// # pub trait Deserialize {}
100-
/// #
101-
/// # mod serde_json {
102-
/// # use super::Deserialize;
103-
/// # use std::io;
104-
/// #
105-
/// # pub fn from_str<T: Deserialize>(json: &str) -> io::Result<T> {
106-
/// # unimplemented!()
107-
/// # }
108-
/// # }
109-
/// #
110-
/// # #[derive(Debug)]
111-
/// # struct ClusterMap;
112-
/// #
113-
/// # impl Deserialize for ClusterMap {}
114-
/// #
115-
/// use lexarg_error::Result;
116-
///
117-
/// fn main() -> Result<()> {
118-
/// # return Ok(());
119-
/// let config = std::fs::read_to_string("cluster.json")?;
120-
/// let map: ClusterMap = serde_json::from_str(&config)?;
121-
/// println!("cluster info: {:#?}", map);
122-
/// Ok(())
123-
/// }
124-
/// ```
22+
/// `Result` that defaults to [`Error`]
12523
pub type Result<T, E = Error> = std::result::Result<T, E>;
12624

12725
/// Argument error type for use with lexarg
128-
#[derive(Debug)]
12926
pub struct Error {
13027
msg: String,
13128
}
@@ -137,24 +34,105 @@ impl Error {
13734
where
13835
M: std::fmt::Display,
13936
{
140-
Error {
37+
Self {
14138
msg: message.to_string(),
14239
}
14340
}
14441
}
14542

146-
impl<E> From<E> for Error
43+
impl From<ErrorContext<'_>> for Error {
44+
#[cold]
45+
fn from(error: ErrorContext<'_>) -> Self {
46+
Self::msg(error.to_string())
47+
}
48+
}
49+
50+
impl std::fmt::Debug for Error {
51+
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52+
self.msg.fmt(formatter)
53+
}
54+
}
55+
56+
impl std::fmt::Display for Error {
57+
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58+
self.msg.fmt(formatter)
59+
}
60+
}
61+
62+
/// Collect context for creating an [`Error`]
63+
#[derive(Debug)]
64+
pub struct ErrorContext<'a> {
65+
msg: String,
66+
within: Option<lexarg::Arg<'a>>,
67+
unexpected: Option<lexarg::Arg<'a>>,
68+
}
69+
70+
impl<'a> ErrorContext<'a> {
71+
/// Create a new error object from a printable error message.
72+
#[cold]
73+
pub fn msg<M>(message: M) -> Self
74+
where
75+
M: std::fmt::Display,
76+
{
77+
Self {
78+
msg: message.to_string(),
79+
within: None,
80+
unexpected: None,
81+
}
82+
}
83+
84+
/// [`Arg`][lexarg::Arg] the error occurred within
85+
#[cold]
86+
pub fn within(mut self, within: lexarg::Arg<'a>) -> Self {
87+
self.within = Some(within);
88+
self
89+
}
90+
91+
/// The failing [`Arg`][lexarg::Arg]
92+
#[cold]
93+
pub fn unexpected(mut self, unexpected: lexarg::Arg<'a>) -> Self {
94+
self.unexpected = Some(unexpected);
95+
self
96+
}
97+
}
98+
99+
impl<E> From<E> for ErrorContext<'_>
147100
where
148101
E: std::error::Error + Send + Sync + 'static,
149102
{
150103
#[cold]
151104
fn from(error: E) -> Self {
152-
Error::msg(error)
105+
Self::msg(error)
153106
}
154107
}
155108

156-
impl std::fmt::Display for Error {
109+
impl std::fmt::Display for ErrorContext<'_> {
157110
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158-
self.msg.fmt(formatter)
111+
self.msg.fmt(formatter)?;
112+
if let Some(unexpected) = &self.unexpected {
113+
write!(formatter, ", found `")?;
114+
match unexpected {
115+
lexarg::Arg::Short(short) => write!(formatter, "-{short}")?,
116+
lexarg::Arg::Long(long) => write!(formatter, "--{long}")?,
117+
lexarg::Arg::Escape(value) => write!(formatter, "{value}")?,
118+
lexarg::Arg::Value(value) | lexarg::Arg::Unexpected(value) => {
119+
write!(formatter, "{}", value.to_string_lossy())?;
120+
}
121+
}
122+
write!(formatter, "`")?;
123+
}
124+
if let Some(within) = &self.within {
125+
write!(formatter, " when parsing `")?;
126+
match within {
127+
lexarg::Arg::Short(short) => write!(formatter, "-{short}")?,
128+
lexarg::Arg::Long(long) => write!(formatter, "--{long}")?,
129+
lexarg::Arg::Escape(value) => write!(formatter, "{value}")?,
130+
lexarg::Arg::Value(value) | lexarg::Arg::Unexpected(value) => {
131+
write!(formatter, "{}", value.to_string_lossy())?;
132+
}
133+
}
134+
write!(formatter, "`")?;
135+
}
136+
Ok(())
159137
}
160138
}

0 commit comments

Comments
 (0)