From cd0aa8903674e914529632eb63abd5470d49d9fb Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 5 Mar 2025 13:59:29 -0600 Subject: [PATCH 01/35] docs(lexarg): Clean up the docs --- crates/lexarg/src/lib.rs | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/crates/lexarg/src/lib.rs b/crates/lexarg/src/lib.rs index 6dc1052..6894624 100644 --- a/crates/lexarg/src/lib.rs +++ b/crates/lexarg/src/lib.rs @@ -120,12 +120,9 @@ impl<'a> Parser<'a> { self.next_raw() } - /// Get the next option or positional argument. + /// Get the next option or positional [`Arg`]. /// - /// A return value of `Ok(None)` means the command line has been exhausted. - /// - /// Options that are not valid unicode are transformed with replacement - /// characters as by [`String::from_utf8_lossy`]. + /// A return value of `None` means the command line has been exhausted. #[allow(clippy::should_implement_trait)] pub fn next(&mut self) -> Option> { // Always reset @@ -139,7 +136,7 @@ impl<'a> Parser<'a> { Some(Arg::Unexpected(attached)) } Some(State::PendingShorts(valid, invalid, index)) => { - // We're somewhere inside a -abc chain. Because we're in .next(), not .flag_value(), we + // We're somewhere inside a `-abc` chain. Because we're in `.next()`, not `.flag_value()`, we // can assume that the next character is another option. let unparsed = &valid[index..]; let mut char_indices = unparsed.char_indices(); @@ -406,14 +403,14 @@ where #[derive(Debug, Copy, Clone, PartialEq, Eq)] enum State<'a> { - /// We have a value left over from --option=value. + /// We have a value left over from `--option=value` PendingValue(&'a OsStr), - /// We're in the middle of -abc. + /// We're in the middle of `-abc` /// /// On Windows and other non-UTF8-OsString platforms this Vec should /// only ever contain valid UTF-8 (and could instead be a String). PendingShorts(&'a str, &'a OsStr, usize), - /// We saw -- and know no more options are coming. + /// We saw `--` and know no more options are coming. Escaped, } @@ -427,14 +424,16 @@ impl State<'_> { } } -/// A command line argument found by [`Parser`], either an option or a positional argument. +/// A command line argument found by [`Parser`], either an option or a positional argument #[derive(Debug, Clone, PartialEq, Eq)] pub enum Arg<'a> { - /// A short option, e.g. `Short('q')` for `-q`. + /// A short option, e.g. `Short('q')` for `-q` Short(char), - /// A long option, e.g. `Long("verbose")` for `--verbose`. (The dashes are not included.) + /// A long option, e.g. `Long("verbose")` for `--verbose` + /// + /// The dashes are not included Long(&'a str), - /// A positional argument, e.g. `/dev/null`. + /// A positional argument, e.g. `/dev/null` Value(&'a OsStr), /// Marks the following values have been escaped with `--` Escape, From dcf8339741c78e5c595d9d7c09cf0acdc8e0e5c5 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 5 Mar 2025 14:05:19 -0600 Subject: [PATCH 02/35] fix(lexarg)!: Clarify Parser::next works with `Arg` --- crates/lexarg-error/src/lib.rs | 2 +- crates/lexarg/src/lib.rs | 197 ++++++++++++------------- crates/libtest2-harness/src/harness.rs | 2 +- 3 files changed, 100 insertions(+), 101 deletions(-) diff --git a/crates/lexarg-error/src/lib.rs b/crates/lexarg-error/src/lib.rs index 4ce37f8..4c682b0 100644 --- a/crates/lexarg-error/src/lib.rs +++ b/crates/lexarg-error/src/lib.rs @@ -23,7 +23,7 @@ //! let mut raw = std::env::args_os().collect::>(); //! let mut parser = lexarg::Parser::new(&raw); //! parser.bin(); -//! while let Some(arg) = parser.next() { +//! while let Some(arg) = parser.next_arg() { //! match arg { //! Short('n') | Long("number") => { //! number = parser diff --git a/crates/lexarg/src/lib.rs b/crates/lexarg/src/lib.rs index 6894624..8f1a72f 100644 --- a/crates/lexarg/src/lib.rs +++ b/crates/lexarg/src/lib.rs @@ -20,7 +20,7 @@ //! let mut raw = std::env::args_os().collect::>(); //! let mut parser = lexarg::Parser::new(&raw); //! parser.bin(); -//! while let Some(arg) = parser.next() { +//! while let Some(arg) = parser.next_arg() { //! match arg { //! Short('n') | Long("number") => { //! number = parser @@ -123,8 +123,7 @@ impl<'a> Parser<'a> { /// Get the next option or positional [`Arg`]. /// /// A return value of `None` means the command line has been exhausted. - #[allow(clippy::should_implement_trait)] - pub fn next(&mut self) -> Option> { + pub fn next_arg(&mut self) -> Option> { // Always reset self.was_attached = false; @@ -136,7 +135,7 @@ impl<'a> Parser<'a> { Some(Arg::Unexpected(attached)) } Some(State::PendingShorts(valid, invalid, index)) => { - // We're somewhere inside a `-abc` chain. Because we're in `.next()`, not `.flag_value()`, we + // We're somewhere inside a `-abc` chain. Because we're in `.next_arg()`, not `.flag_value()`, we // can assume that the next character is another option. let unparsed = &valid[index..]; let mut char_indices = unparsed.char_indices(); @@ -232,7 +231,7 @@ impl<'a> Parser<'a> { let (valid, invalid) = split_nonutf8_once(arg); let invalid = invalid.unwrap_or_default(); self.state = Some(State::PendingShorts(valid, invalid, 1)); - self.next() + self.next_arg() } else { self.state = None; self.current += 1; @@ -245,7 +244,7 @@ impl<'a> Parser<'a> { /// Get a flag's value /// /// This function should normally be called right after seeing a flag that expects a value; - /// positional arguments should be collected with [`Parser::next()`]. + /// positional arguments should be collected with [`Parser::next_arg()`]. /// /// A value is collected even if it looks like an option (i.e., starts with `-`). /// @@ -470,83 +469,83 @@ mod tests { #[test] fn test_basic() { let mut p = Parser::new(&["-n", "10", "foo", "-", "--", "baz", "-qux"]); - assert_eq!(p.next().unwrap(), Short('n')); + assert_eq!(p.next_arg().unwrap(), Short('n')); assert_eq!(p.flag_value().unwrap(), "10"); - assert_eq!(p.next().unwrap(), Value(OsStr::new("foo"))); - assert_eq!(p.next().unwrap(), Value(OsStr::new("-"))); - assert_eq!(p.next().unwrap(), Escape); - assert_eq!(p.next().unwrap(), Value(OsStr::new("baz"))); - assert_eq!(p.next().unwrap(), Value(OsStr::new("-qux"))); - assert_eq!(p.next(), None); - assert_eq!(p.next(), None); - assert_eq!(p.next(), None); + assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("foo"))); + assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-"))); + assert_eq!(p.next_arg().unwrap(), Escape); + assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("baz"))); + assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-qux"))); + assert_eq!(p.next_arg(), None); + assert_eq!(p.next_arg(), None); + assert_eq!(p.next_arg(), None); } #[test] fn test_combined() { let mut p = Parser::new(&["-abc", "-fvalue", "-xfvalue"]); - assert_eq!(p.next().unwrap(), Short('a')); - assert_eq!(p.next().unwrap(), Short('b')); - assert_eq!(p.next().unwrap(), Short('c')); - assert_eq!(p.next().unwrap(), Short('f')); + assert_eq!(p.next_arg().unwrap(), Short('a')); + assert_eq!(p.next_arg().unwrap(), Short('b')); + assert_eq!(p.next_arg().unwrap(), Short('c')); + assert_eq!(p.next_arg().unwrap(), Short('f')); assert_eq!(p.flag_value().unwrap(), "value"); - assert_eq!(p.next().unwrap(), Short('x')); - assert_eq!(p.next().unwrap(), Short('f')); + assert_eq!(p.next_arg().unwrap(), Short('x')); + assert_eq!(p.next_arg().unwrap(), Short('f')); assert_eq!(p.flag_value().unwrap(), "value"); - assert_eq!(p.next(), None); + assert_eq!(p.next_arg(), None); } #[test] fn test_long() { let mut p = Parser::new(&["--foo", "--bar=qux", "--foobar=qux=baz"]); - assert_eq!(p.next().unwrap(), Long("foo")); - assert_eq!(p.next().unwrap(), Long("bar")); + assert_eq!(p.next_arg().unwrap(), Long("foo")); + assert_eq!(p.next_arg().unwrap(), Long("bar")); assert_eq!(p.flag_value().unwrap(), "qux"); assert_eq!(p.flag_value(), None); - assert_eq!(p.next().unwrap(), Long("foobar")); - assert_eq!(p.next().unwrap(), Unexpected(OsStr::new("qux=baz"))); - assert_eq!(p.next(), None); + assert_eq!(p.next_arg().unwrap(), Long("foobar")); + assert_eq!(p.next_arg().unwrap(), Unexpected(OsStr::new("qux=baz"))); + assert_eq!(p.next_arg(), None); } #[test] fn test_dash_args() { // "--" should indicate the end of the options let mut p = Parser::new(&["-x", "--", "-y"]); - assert_eq!(p.next().unwrap(), Short('x')); - assert_eq!(p.next().unwrap(), Escape); - assert_eq!(p.next().unwrap(), Value(OsStr::new("-y"))); - assert_eq!(p.next(), None); + assert_eq!(p.next_arg().unwrap(), Short('x')); + assert_eq!(p.next_arg().unwrap(), Escape); + assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-y"))); + assert_eq!(p.next_arg(), None); // ...even if it's an argument of an option let mut p = Parser::new(&["-x", "--", "-y"]); - assert_eq!(p.next().unwrap(), Short('x')); + assert_eq!(p.next_arg().unwrap(), Short('x')); assert_eq!(p.flag_value(), None); - assert_eq!(p.next().unwrap(), Value(OsStr::new("-y"))); - assert_eq!(p.next(), None); + assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-y"))); + assert_eq!(p.next_arg(), None); // "-" is a valid value that should not be treated as an option let mut p = Parser::new(&["-x", "-", "-y"]); - assert_eq!(p.next().unwrap(), Short('x')); - assert_eq!(p.next().unwrap(), Value(OsStr::new("-"))); - assert_eq!(p.next().unwrap(), Short('y')); - assert_eq!(p.next(), None); + assert_eq!(p.next_arg().unwrap(), Short('x')); + assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-"))); + assert_eq!(p.next_arg().unwrap(), Short('y')); + assert_eq!(p.next_arg(), None); // '-' is a silly and hard to use short option, but other parsers treat // it like an option in this position let mut p = Parser::new(&["-x-y"]); - assert_eq!(p.next().unwrap(), Short('x')); - assert_eq!(p.next().unwrap(), Unexpected(OsStr::new("-y"))); - assert_eq!(p.next(), None); + assert_eq!(p.next_arg().unwrap(), Short('x')); + assert_eq!(p.next_arg().unwrap(), Unexpected(OsStr::new("-y"))); + assert_eq!(p.next_arg(), None); } #[test] fn test_missing_value() { let mut p = Parser::new(&["-o"]); - assert_eq!(p.next().unwrap(), Short('o')); + assert_eq!(p.next_arg().unwrap(), Short('o')); assert_eq!(p.flag_value(), None); let mut q = Parser::new(&["--out"]); - assert_eq!(q.next().unwrap(), Long("out")); + assert_eq!(q.next_arg().unwrap(), Long("out")); assert_eq!(q.flag_value(), None); let args: [&OsStr; 0] = []; @@ -559,37 +558,37 @@ mod tests { let mut p = Parser::new(&[ "--=", "--=3", "-", "-x", "--", "-", "-x", "--", "", "-", "-x", ]); - assert_eq!(p.next().unwrap(), Unexpected(OsStr::new("--="))); - assert_eq!(p.next().unwrap(), Unexpected(OsStr::new("--=3"))); - assert_eq!(p.next().unwrap(), Value(OsStr::new("-"))); - assert_eq!(p.next().unwrap(), Short('x')); - assert_eq!(p.next().unwrap(), Escape); - assert_eq!(p.next().unwrap(), Value(OsStr::new("-"))); - assert_eq!(p.next().unwrap(), Value(OsStr::new("-x"))); - assert_eq!(p.next().unwrap(), Value(OsStr::new("--"))); - assert_eq!(p.next().unwrap(), Value(OsStr::new(""))); - assert_eq!(p.next().unwrap(), Value(OsStr::new("-"))); - assert_eq!(p.next().unwrap(), Value(OsStr::new("-x"))); - assert_eq!(p.next(), None); + assert_eq!(p.next_arg().unwrap(), Unexpected(OsStr::new("--="))); + assert_eq!(p.next_arg().unwrap(), Unexpected(OsStr::new("--=3"))); + assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-"))); + assert_eq!(p.next_arg().unwrap(), Short('x')); + assert_eq!(p.next_arg().unwrap(), Escape); + assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-"))); + assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-x"))); + assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("--"))); + assert_eq!(p.next_arg().unwrap(), Value(OsStr::new(""))); + assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-"))); + assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-x"))); + assert_eq!(p.next_arg(), None); let bad = bad_string("--=@"); let args = [&bad]; let mut q = Parser::new(&args); - assert_eq!(q.next().unwrap(), Unexpected(OsStr::new(&bad))); + assert_eq!(q.next_arg().unwrap(), Unexpected(OsStr::new(&bad))); let mut r = Parser::new(&[""]); - assert_eq!(r.next().unwrap(), Value(OsStr::new(""))); + assert_eq!(r.next_arg().unwrap(), Value(OsStr::new(""))); } #[test] fn test_unicode() { let mut p = Parser::new(&["-aµ", "--µ=10", "µ", "--foo=µ"]); - assert_eq!(p.next().unwrap(), Short('a')); - assert_eq!(p.next().unwrap(), Short('µ')); - assert_eq!(p.next().unwrap(), Long("µ")); + assert_eq!(p.next_arg().unwrap(), Short('a')); + assert_eq!(p.next_arg().unwrap(), Short('µ')); + assert_eq!(p.next_arg().unwrap(), Long("µ")); assert_eq!(p.flag_value().unwrap(), "10"); - assert_eq!(p.next().unwrap(), Value(OsStr::new("µ"))); - assert_eq!(p.next().unwrap(), Long("foo")); + assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("µ"))); + assert_eq!(p.next_arg().unwrap(), Long("foo")); assert_eq!(p.flag_value().unwrap(), "µ"); } @@ -598,23 +597,23 @@ mod tests { fn test_mixed_invalid() { let args = [bad_string("--foo=@@@")]; let mut p = Parser::new(&args); - assert_eq!(p.next().unwrap(), Long("foo")); + assert_eq!(p.next_arg().unwrap(), Long("foo")); assert_eq!(p.flag_value().unwrap(), bad_string("@@@")); let args = [bad_string("-💣@@@")]; let mut q = Parser::new(&args); - assert_eq!(q.next().unwrap(), Short('💣')); + assert_eq!(q.next_arg().unwrap(), Short('💣')); assert_eq!(q.flag_value().unwrap(), bad_string("@@@")); let args = [bad_string("-f@@@")]; let mut r = Parser::new(&args); - assert_eq!(r.next().unwrap(), Short('f')); - assert_eq!(r.next().unwrap(), Unexpected(&bad_string("@@@"))); - assert_eq!(r.next(), None); + assert_eq!(r.next_arg().unwrap(), Short('f')); + assert_eq!(r.next_arg().unwrap(), Unexpected(&bad_string("@@@"))); + assert_eq!(r.next_arg(), None); let args = [bad_string("--foo=bar=@@@")]; let mut s = Parser::new(&args); - assert_eq!(s.next().unwrap(), Long("foo")); + assert_eq!(s.next_arg().unwrap(), Long("foo")); assert_eq!(s.flag_value().unwrap(), bad_string("bar=@@@")); } @@ -623,7 +622,7 @@ mod tests { fn test_separate_invalid() { let args = [bad_string("--foo"), bad_string("@@@")]; let mut p = Parser::new(&args); - assert_eq!(p.next().unwrap(), Long("foo")); + assert_eq!(p.next_arg().unwrap(), Long("foo")); assert_eq!(p.flag_value().unwrap(), bad_string("@@@")); } @@ -632,13 +631,13 @@ mod tests { fn test_invalid_long_option() { let args = [bad_string("--@=10")]; let mut p = Parser::new(&args); - assert_eq!(p.next().unwrap(), Unexpected(&args[0])); - assert_eq!(p.next(), None); + assert_eq!(p.next_arg().unwrap(), Unexpected(&args[0])); + assert_eq!(p.next_arg(), None); let args = [bad_string("--@")]; let mut p = Parser::new(&args); - assert_eq!(p.next().unwrap(), Unexpected(&args[0])); - assert_eq!(p.next(), None); + assert_eq!(p.next_arg().unwrap(), Unexpected(&args[0])); + assert_eq!(p.next_arg(), None); } #[cfg(any(unix, target_os = "wasi", windows))] @@ -646,46 +645,46 @@ mod tests { fn test_invalid_short_option() { let args = [bad_string("-@")]; let mut p = Parser::new(&args); - assert_eq!(p.next().unwrap(), Unexpected(&args[0])); - assert_eq!(p.next(), None); + assert_eq!(p.next_arg().unwrap(), Unexpected(&args[0])); + assert_eq!(p.next_arg(), None); } #[test] fn short_opt_equals_sign() { let mut p = Parser::new(&["-a=b"]); - assert_eq!(p.next().unwrap(), Short('a')); + assert_eq!(p.next_arg().unwrap(), Short('a')); assert_eq!(p.flag_value().unwrap(), OsStr::new("b")); - assert_eq!(p.next(), None); + assert_eq!(p.next_arg(), None); let mut p = Parser::new(&["-a=b", "c"]); - assert_eq!(p.next().unwrap(), Short('a')); + assert_eq!(p.next_arg().unwrap(), Short('a')); assert_eq!(p.flag_value().unwrap(), OsStr::new("b")); assert_eq!(p.flag_value(), None); - assert_eq!(p.next().unwrap(), Value(OsStr::new("c"))); - assert_eq!(p.next(), None); + assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("c"))); + assert_eq!(p.next_arg(), None); let mut p = Parser::new(&["-a=b"]); - assert_eq!(p.next().unwrap(), Short('a')); - assert_eq!(p.next().unwrap(), Unexpected(OsStr::new("b"))); - assert_eq!(p.next(), None); + assert_eq!(p.next_arg().unwrap(), Short('a')); + assert_eq!(p.next_arg().unwrap(), Unexpected(OsStr::new("b"))); + assert_eq!(p.next_arg(), None); let mut p = Parser::new(&["-a="]); - assert_eq!(p.next().unwrap(), Short('a')); + assert_eq!(p.next_arg().unwrap(), Short('a')); assert_eq!(p.flag_value().unwrap(), OsStr::new("")); - assert_eq!(p.next(), None); + assert_eq!(p.next_arg(), None); let mut p = Parser::new(&["-a="]); - assert_eq!(p.next().unwrap(), Short('a')); - assert_eq!(p.next().unwrap(), Unexpected(OsStr::new(""))); - assert_eq!(p.next(), None); + assert_eq!(p.next_arg().unwrap(), Short('a')); + assert_eq!(p.next_arg().unwrap(), Unexpected(OsStr::new(""))); + assert_eq!(p.next_arg(), None); let mut p = Parser::new(&["-="]); - assert_eq!(p.next().unwrap(), Unexpected(OsStr::new("-="))); - assert_eq!(p.next(), None); + assert_eq!(p.next_arg().unwrap(), Unexpected(OsStr::new("-="))); + assert_eq!(p.next_arg(), None); let mut p = Parser::new(&["-=a"]); - assert_eq!(p.next().unwrap(), Unexpected(OsStr::new("-=a"))); - assert_eq!(p.next(), None); + assert_eq!(p.next_arg().unwrap(), Unexpected(OsStr::new("-=a"))); + assert_eq!(p.next_arg(), None); } #[cfg(any(unix, target_os = "wasi", windows))] @@ -694,14 +693,14 @@ mod tests { let bad = bad_string("@"); let args = [bad_string("-a=@")]; let mut p = Parser::new(&args); - assert_eq!(p.next().unwrap(), Short('a')); + assert_eq!(p.next_arg().unwrap(), Short('a')); assert_eq!(p.flag_value().unwrap(), bad_string("@")); - assert_eq!(p.next(), None); + assert_eq!(p.next_arg(), None); let mut p = Parser::new(&args); - assert_eq!(p.next().unwrap(), Short('a')); - assert_eq!(p.next().unwrap(), Unexpected(&bad)); - assert_eq!(p.next(), None); + assert_eq!(p.next_arg().unwrap(), Short('a')); + assert_eq!(p.next_arg().unwrap(), Unexpected(&bad)); + assert_eq!(p.next_arg(), None); } /// Transform @ characters into invalid unicode. @@ -799,7 +798,7 @@ mod tests { if parser.has_pending() { { let mut parser = parser.clone(); - let next = parser.next(); + let next = parser.next_arg(); assert!( matches!(next, Some(Unexpected(_)) | Some(Short(_))), "{next:?} via {path:?}", @@ -820,7 +819,7 @@ mod tests { } else { { let mut parser = parser.clone(); - let next = parser.next(); + let next = parser.next_arg(); match &next { None => { assert!( diff --git a/crates/libtest2-harness/src/harness.rs b/crates/libtest2-harness/src/harness.rs index 7840ef4..ce863fe 100644 --- a/crates/libtest2-harness/src/harness.rs +++ b/crates/libtest2-harness/src/harness.rs @@ -74,7 +74,7 @@ fn parse(parser: &mut cli::Parser<'_>) -> cli::Result let mut test_opts = libtest_lexarg::TestOptsParseState::new(); let bin = parser.bin(); - while let Some(arg) = parser.next() { + while let Some(arg) = parser.next_arg() { match arg { cli::Arg::Short('h') | cli::Arg::Long("help") => { let bin = bin From 17a3c2c42ee262e9a3e86dd9b9a01b985850e055 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 5 Mar 2025 15:12:00 -0600 Subject: [PATCH 03/35] refactor(lexarg): Clarify next_value isn't general --- crates/lexarg/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/lexarg/src/lib.rs b/crates/lexarg/src/lib.rs index 8f1a72f..18facb4 100644 --- a/crates/lexarg/src/lib.rs +++ b/crates/lexarg/src/lib.rs @@ -259,7 +259,7 @@ impl<'a> Parser<'a> { } if !self.was_attached { - return self.next_value(); + return self.next_detached_value(); } None @@ -295,7 +295,7 @@ impl<'a> Parser<'a> { } } - fn next_value(&mut self) -> Option<&'a OsStr> { + fn next_detached_value(&mut self) -> Option<&'a OsStr> { if self.state == Some(State::Escaped) { // Escaped values are positional-only return None; From b9222d5ccd5f0bcf9487df27ff8d3185280b71b4 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 5 Mar 2025 15:13:28 -0600 Subject: [PATCH 04/35] refactor(lexarg): Clarify how flag_value works --- crates/lexarg/src/lib.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/crates/lexarg/src/lib.rs b/crates/lexarg/src/lib.rs index 18facb4..7d73a5a 100644 --- a/crates/lexarg/src/lib.rs +++ b/crates/lexarg/src/lib.rs @@ -253,16 +253,14 @@ impl<'a> Parser<'a> { /// - `--` was encountered, meaning all remaining arguments are positional /// - Being called again when the first value was attached (e.g. `--hello=world`) pub fn flag_value(&mut self) -> Option<&'a OsStr> { - if let Some(value) = self.next_attached_value() { + if self.was_attached { + None + } else if let Some(value) = self.next_attached_value() { self.was_attached = true; - return Some(value); - } - - if !self.was_attached { - return self.next_detached_value(); + Some(value) + } else { + self.next_detached_value() } - - None } fn next_attached_value(&mut self) -> Option<&'a OsStr> { From 6767504f954d5da5d685a3e7727d28321320be28 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 5 Mar 2025 15:27:09 -0600 Subject: [PATCH 05/35] fix(lexarg)!: Clarify Parser::flag_value advances --- crates/lexarg-error/src/lib.rs | 2 +- crates/lexarg/src/lib.rs | 54 +++++++++++++++++--------------- crates/libtest-lexarg/src/lib.rs | 14 ++++----- 3 files changed, 36 insertions(+), 34 deletions(-) diff --git a/crates/lexarg-error/src/lib.rs b/crates/lexarg-error/src/lib.rs index 4c682b0..9bdd201 100644 --- a/crates/lexarg-error/src/lib.rs +++ b/crates/lexarg-error/src/lib.rs @@ -27,7 +27,7 @@ //! match arg { //! Short('n') | Long("number") => { //! number = parser -//! .flag_value().ok_or_else(|| Error::msg("`--number` requires a value"))? +//! .next_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))?; //! } diff --git a/crates/lexarg/src/lib.rs b/crates/lexarg/src/lib.rs index 7d73a5a..f036095 100644 --- a/crates/lexarg/src/lib.rs +++ b/crates/lexarg/src/lib.rs @@ -24,7 +24,7 @@ //! match arg { //! Short('n') | Long("number") => { //! number = parser -//! .flag_value().ok_or("`--number` requires a value")? +//! .next_flag_value().ok_or("`--number` requires a value")? //! .to_str().ok_or("invalid number")? //! .parse().map_err(|_e| "invalid number")?; //! } @@ -122,7 +122,9 @@ impl<'a> Parser<'a> { /// Get the next option or positional [`Arg`]. /// - /// A return value of `None` means the command line has been exhausted. + /// Returns `None` if the command line has been exhausted. + /// + /// Returns [`Arg::Unexpected`] on failure pub fn next_arg(&mut self) -> Option> { // Always reset self.was_attached = false; @@ -135,7 +137,7 @@ impl<'a> Parser<'a> { Some(Arg::Unexpected(attached)) } Some(State::PendingShorts(valid, invalid, index)) => { - // We're somewhere inside a `-abc` chain. Because we're in `.next_arg()`, not `.flag_value()`, we + // We're somewhere inside a `-abc` chain. Because we're in `.next_arg()`, not `.next_flag_value()`, we // can assume that the next character is another option. let unparsed = &valid[index..]; let mut char_indices = unparsed.char_indices(); @@ -252,7 +254,7 @@ impl<'a> Parser<'a> { /// - No more arguments are present /// - `--` was encountered, meaning all remaining arguments are positional /// - Being called again when the first value was attached (e.g. `--hello=world`) - pub fn flag_value(&mut self) -> Option<&'a OsStr> { + pub fn next_flag_value(&mut self) -> Option<&'a OsStr> { if self.was_attached { None } else if let Some(value) = self.next_attached_value() { @@ -468,7 +470,7 @@ mod tests { fn test_basic() { let mut p = Parser::new(&["-n", "10", "foo", "-", "--", "baz", "-qux"]); assert_eq!(p.next_arg().unwrap(), Short('n')); - assert_eq!(p.flag_value().unwrap(), "10"); + assert_eq!(p.next_flag_value().unwrap(), "10"); assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("foo"))); assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-"))); assert_eq!(p.next_arg().unwrap(), Escape); @@ -486,10 +488,10 @@ mod tests { assert_eq!(p.next_arg().unwrap(), Short('b')); assert_eq!(p.next_arg().unwrap(), Short('c')); assert_eq!(p.next_arg().unwrap(), Short('f')); - assert_eq!(p.flag_value().unwrap(), "value"); + assert_eq!(p.next_flag_value().unwrap(), "value"); assert_eq!(p.next_arg().unwrap(), Short('x')); assert_eq!(p.next_arg().unwrap(), Short('f')); - assert_eq!(p.flag_value().unwrap(), "value"); + assert_eq!(p.next_flag_value().unwrap(), "value"); assert_eq!(p.next_arg(), None); } @@ -498,8 +500,8 @@ mod tests { let mut p = Parser::new(&["--foo", "--bar=qux", "--foobar=qux=baz"]); assert_eq!(p.next_arg().unwrap(), Long("foo")); assert_eq!(p.next_arg().unwrap(), Long("bar")); - assert_eq!(p.flag_value().unwrap(), "qux"); - assert_eq!(p.flag_value(), None); + assert_eq!(p.next_flag_value().unwrap(), "qux"); + assert_eq!(p.next_flag_value(), None); assert_eq!(p.next_arg().unwrap(), Long("foobar")); assert_eq!(p.next_arg().unwrap(), Unexpected(OsStr::new("qux=baz"))); assert_eq!(p.next_arg(), None); @@ -517,7 +519,7 @@ mod tests { // ...even if it's an argument of an option let mut p = Parser::new(&["-x", "--", "-y"]); assert_eq!(p.next_arg().unwrap(), Short('x')); - assert_eq!(p.flag_value(), None); + assert_eq!(p.next_flag_value(), None); assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-y"))); assert_eq!(p.next_arg(), None); @@ -540,15 +542,15 @@ mod tests { fn test_missing_value() { let mut p = Parser::new(&["-o"]); assert_eq!(p.next_arg().unwrap(), Short('o')); - assert_eq!(p.flag_value(), None); + assert_eq!(p.next_flag_value(), None); let mut q = Parser::new(&["--out"]); assert_eq!(q.next_arg().unwrap(), Long("out")); - assert_eq!(q.flag_value(), None); + assert_eq!(q.next_flag_value(), None); let args: [&OsStr; 0] = []; let mut r = Parser::new(&args); - assert_eq!(r.flag_value(), None); + assert_eq!(r.next_flag_value(), None); } #[test] @@ -584,10 +586,10 @@ mod tests { assert_eq!(p.next_arg().unwrap(), Short('a')); assert_eq!(p.next_arg().unwrap(), Short('µ')); assert_eq!(p.next_arg().unwrap(), Long("µ")); - assert_eq!(p.flag_value().unwrap(), "10"); + assert_eq!(p.next_flag_value().unwrap(), "10"); assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("µ"))); assert_eq!(p.next_arg().unwrap(), Long("foo")); - assert_eq!(p.flag_value().unwrap(), "µ"); + assert_eq!(p.next_flag_value().unwrap(), "µ"); } #[cfg(any(unix, target_os = "wasi", windows))] @@ -596,12 +598,12 @@ mod tests { let args = [bad_string("--foo=@@@")]; let mut p = Parser::new(&args); assert_eq!(p.next_arg().unwrap(), Long("foo")); - assert_eq!(p.flag_value().unwrap(), bad_string("@@@")); + assert_eq!(p.next_flag_value().unwrap(), bad_string("@@@")); let args = [bad_string("-💣@@@")]; let mut q = Parser::new(&args); assert_eq!(q.next_arg().unwrap(), Short('💣')); - assert_eq!(q.flag_value().unwrap(), bad_string("@@@")); + assert_eq!(q.next_flag_value().unwrap(), bad_string("@@@")); let args = [bad_string("-f@@@")]; let mut r = Parser::new(&args); @@ -612,7 +614,7 @@ mod tests { let args = [bad_string("--foo=bar=@@@")]; let mut s = Parser::new(&args); assert_eq!(s.next_arg().unwrap(), Long("foo")); - assert_eq!(s.flag_value().unwrap(), bad_string("bar=@@@")); + assert_eq!(s.next_flag_value().unwrap(), bad_string("bar=@@@")); } #[cfg(any(unix, target_os = "wasi", windows))] @@ -621,7 +623,7 @@ mod tests { let args = [bad_string("--foo"), bad_string("@@@")]; let mut p = Parser::new(&args); assert_eq!(p.next_arg().unwrap(), Long("foo")); - assert_eq!(p.flag_value().unwrap(), bad_string("@@@")); + assert_eq!(p.next_flag_value().unwrap(), bad_string("@@@")); } #[cfg(any(unix, target_os = "wasi", windows))] @@ -651,13 +653,13 @@ mod tests { fn short_opt_equals_sign() { let mut p = Parser::new(&["-a=b"]); assert_eq!(p.next_arg().unwrap(), Short('a')); - assert_eq!(p.flag_value().unwrap(), OsStr::new("b")); + assert_eq!(p.next_flag_value().unwrap(), OsStr::new("b")); assert_eq!(p.next_arg(), None); let mut p = Parser::new(&["-a=b", "c"]); assert_eq!(p.next_arg().unwrap(), Short('a')); - assert_eq!(p.flag_value().unwrap(), OsStr::new("b")); - assert_eq!(p.flag_value(), None); + assert_eq!(p.next_flag_value().unwrap(), OsStr::new("b")); + assert_eq!(p.next_flag_value(), None); assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("c"))); assert_eq!(p.next_arg(), None); @@ -668,7 +670,7 @@ mod tests { let mut p = Parser::new(&["-a="]); assert_eq!(p.next_arg().unwrap(), Short('a')); - assert_eq!(p.flag_value().unwrap(), OsStr::new("")); + assert_eq!(p.next_flag_value().unwrap(), OsStr::new("")); assert_eq!(p.next_arg(), None); let mut p = Parser::new(&["-a="]); @@ -692,7 +694,7 @@ mod tests { let args = [bad_string("-a=@")]; let mut p = Parser::new(&args); assert_eq!(p.next_arg().unwrap(), Short('a')); - assert_eq!(p.flag_value().unwrap(), bad_string("@")); + assert_eq!(p.next_flag_value().unwrap(), bad_string("@")); assert_eq!(p.next_arg(), None); let mut p = Parser::new(&args); @@ -808,7 +810,7 @@ mod tests { { let mut parser = parser.clone(); - let next = parser.flag_value(); + let next = parser.next_flag_value(); assert!(next.is_some(), "{next:?} via {path:?}",); let mut path = path; path.push(format!("pending-value-{next:?}")); @@ -836,7 +838,7 @@ mod tests { { let mut parser = parser.clone(); - let next = parser.flag_value(); + let next = parser.next_flag_value(); match &next { None => { assert!( diff --git a/crates/libtest-lexarg/src/lib.rs b/crates/libtest-lexarg/src/lib.rs index d5f8e4b..f4c0c67 100644 --- a/crates/libtest-lexarg/src/lib.rs +++ b/crates/libtest-lexarg/src/lib.rs @@ -328,7 +328,7 @@ impl TestOptsParseState { } Arg::Long("logfile") => { let path = parser - .flag_value() + .next_flag_value() .ok_or_else(|| Error::msg("`--logfile` requires a path"))?; self.opts.logfile = Some(std::path::PathBuf::from(path)); } @@ -337,7 +337,7 @@ impl TestOptsParseState { } Arg::Long("test-threads") => { let test_threads = parser - .flag_value() + .next_flag_value() .ok_or_else(|| Error::msg("`--test-threads` requires a positive integer"))? .to_str() .ok_or_else(|| Error::msg("unsupported value"))?; @@ -350,7 +350,7 @@ impl TestOptsParseState { } Arg::Long("skip") => { let filter = parser - .flag_value() + .next_flag_value() .ok_or_else(|| Error::msg("`--skip` requires a value"))? .to_str() .ok_or_else(|| Error::msg("unsupported value"))?; @@ -361,7 +361,7 @@ impl TestOptsParseState { } Arg::Long("color") => { let color = parser - .flag_value() + .next_flag_value() .ok_or_else(|| { Error::msg("`--color` requires one of `auto`, `always`, or `never`") })? @@ -383,7 +383,7 @@ impl TestOptsParseState { Arg::Long("format") => { self.quiet = false; let format = parser - .flag_value() + .next_flag_value() .ok_or_else(|| { Error::msg( "`--format` requires one of `pretty`, `terse`, `json`, or `junit`", @@ -408,7 +408,7 @@ impl TestOptsParseState { } Arg::Short('Z') => { let feature = parser - .flag_value() + .next_flag_value() .ok_or_else(|| Error::msg("`-Z` requires a feature name"))? .to_str() .ok_or_else(|| Error::msg("unsupported value"))?; @@ -440,7 +440,7 @@ impl TestOptsParseState { } Arg::Long("shuffle-seed") => { let seed = parser - .flag_value() + .next_flag_value() .ok_or_else(|| Error::msg("`--shuffle-seed` requires a value"))? .to_str() .ok_or_else(|| Error::msg("unsupported value"))? From 5838027aa6c97e0a1864cffb24b2813f786763c5 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 5 Mar 2025 16:46:47 -0600 Subject: [PATCH 06/35] refactor(lexarg): Clarify behavior with asserts --- crates/lexarg/src/lib.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/lexarg/src/lib.rs b/crates/lexarg/src/lib.rs index f036095..d8f9e22 100644 --- a/crates/lexarg/src/lib.rs +++ b/crates/lexarg/src/lib.rs @@ -117,7 +117,7 @@ impl<'a> Parser<'a> { /// Will panic if `next` has been called pub fn bin(&mut self) -> Option<&'a OsStr> { assert_eq!(self.current, 0); - self.next_raw() + self.next_raw_() } /// Get the next option or positional [`Arg`]. @@ -195,7 +195,7 @@ impl<'a> Parser<'a> { } Some(State::Escaped) => { self.state = Some(State::Escaped); - self.next_raw().map(Arg::Value) + self.next_raw_().map(Arg::Value) } None => { let arg = self.raw.get(self.current)?; @@ -256,6 +256,7 @@ impl<'a> Parser<'a> { /// - Being called again when the first value was attached (e.g. `--hello=world`) pub fn next_flag_value(&mut self) -> Option<&'a OsStr> { if self.was_attached { + debug_assert!(!self.has_pending()); None } else if let Some(value) = self.next_attached_value() { self.was_attached = true; @@ -301,7 +302,7 @@ impl<'a> Parser<'a> { return None; } - let next = self.next_raw()?; + let next = self.next_raw_()?; if next == "--" { self.state = Some(State::Escaped); @@ -311,13 +312,15 @@ impl<'a> Parser<'a> { } } - fn next_raw(&mut self) -> Option<&'a OsStr> { + fn next_raw_(&mut self) -> Option<&'a OsStr> { + debug_assert!(!self.has_pending()); + debug_assert!(!self.was_attached); + let next = self.raw.get(self.current)?; self.current += 1; Some(next) } - #[cfg(test)] fn has_pending(&self) -> bool { self.state.as_ref().map(State::has_pending).unwrap_or(false) } @@ -414,7 +417,6 @@ enum State<'a> { } impl State<'_> { - #[cfg(test)] fn has_pending(&self) -> bool { match self { Self::PendingValue(_) | Self::PendingShorts(_, _, _) => true, From 92dd93bb3f2918602c6a4ffa333efd0c54388a73 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 5 Mar 2025 16:56:22 -0600 Subject: [PATCH 07/35] feat(lexarg): Add Parser::next_raw --- crates/lexarg/src/lib.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/lexarg/src/lib.rs b/crates/lexarg/src/lib.rs index d8f9e22..90f5029 100644 --- a/crates/lexarg/src/lib.rs +++ b/crates/lexarg/src/lib.rs @@ -312,6 +312,19 @@ impl<'a> Parser<'a> { } } + /// Get the next argument, independent of what it looks like + /// + /// Returns `Err(())` if an [attached value][Parser::next_attached_value] is present + #[allow(clippy::result_unit_err)] + pub fn next_raw(&mut self) -> Result, ()> { + if self.has_pending() { + Err(()) + } else { + self.was_attached = false; + Ok(self.next_raw_()) + } + } + fn next_raw_(&mut self) -> Option<&'a OsStr> { debug_assert!(!self.has_pending()); debug_assert!(!self.was_attached); From 985214aaad6ec58294d9cfba4da64d8c660d77eb Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 5 Mar 2025 16:58:15 -0600 Subject: [PATCH 08/35] fix(lexarg)!: Remove Parser::bin in favor of Parser::next_raw --- crates/lexarg-error/src/lib.rs | 2 +- crates/lexarg/src/lib.rs | 12 +----------- crates/libtest2-harness/src/harness.rs | 2 +- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/crates/lexarg-error/src/lib.rs b/crates/lexarg-error/src/lib.rs index 9bdd201..bed1699 100644 --- a/crates/lexarg-error/src/lib.rs +++ b/crates/lexarg-error/src/lib.rs @@ -22,7 +22,7 @@ //! let mut shout = false; //! let mut raw = std::env::args_os().collect::>(); //! let mut parser = lexarg::Parser::new(&raw); -//! parser.bin(); +//! let _bin_name = parser.next_raw(); //! while let Some(arg) = parser.next_arg() { //! match arg { //! Short('n') | Long("number") => { diff --git a/crates/lexarg/src/lib.rs b/crates/lexarg/src/lib.rs index 90f5029..6358ca4 100644 --- a/crates/lexarg/src/lib.rs +++ b/crates/lexarg/src/lib.rs @@ -19,7 +19,7 @@ //! let mut shout = false; //! let mut raw = std::env::args_os().collect::>(); //! let mut parser = lexarg::Parser::new(&raw); -//! parser.bin(); +//! let _bin_name = parser.next_raw(); //! while let Some(arg) = parser.next_arg() { //! match arg { //! Short('n') | Long("number") => { @@ -110,16 +110,6 @@ impl<'a> Parser<'a> { } } - /// Extract the binary name before parsing [`Arg`]s - /// - /// # Panic - /// - /// Will panic if `next` has been called - pub fn bin(&mut self) -> Option<&'a OsStr> { - assert_eq!(self.current, 0); - self.next_raw_() - } - /// Get the next option or positional [`Arg`]. /// /// Returns `None` if the command line has been exhausted. diff --git a/crates/libtest2-harness/src/harness.rs b/crates/libtest2-harness/src/harness.rs index ce863fe..ffa6e8a 100644 --- a/crates/libtest2-harness/src/harness.rs +++ b/crates/libtest2-harness/src/harness.rs @@ -73,7 +73,7 @@ const ERROR_EXIT_CODE: i32 = 101; fn parse(parser: &mut cli::Parser<'_>) -> cli::Result { let mut test_opts = libtest_lexarg::TestOptsParseState::new(); - let bin = parser.bin(); + let bin = parser.next_raw().expect("first arg, no pending values"); while let Some(arg) = parser.next_arg() { match arg { cli::Arg::Short('h') | cli::Arg::Long("help") => { From 9dca1eb9ca9e81de16303cc1b9cca9f803a36910 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 6 Mar 2025 12:54:17 -0600 Subject: [PATCH 09/35] fix(lexarg): Allow '-' as a short Unsure why I made this deviation for lexopt --- crates/lexarg/src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/lexarg/src/lib.rs b/crates/lexarg/src/lib.rs index 6358ca4..7444b8d 100644 --- a/crates/lexarg/src/lib.rs +++ b/crates/lexarg/src/lib.rs @@ -132,7 +132,7 @@ impl<'a> Parser<'a> { let unparsed = &valid[index..]; let mut char_indices = unparsed.char_indices(); if let Some((0, short)) = char_indices.next() { - if matches!(short, '=' | '-') { + if short == '=' { let arg = self .raw .get(self.current) @@ -539,7 +539,8 @@ mod tests { // it like an option in this position let mut p = Parser::new(&["-x-y"]); assert_eq!(p.next_arg().unwrap(), Short('x')); - assert_eq!(p.next_arg().unwrap(), Unexpected(OsStr::new("-y"))); + assert_eq!(p.next_arg().unwrap(), Short('-')); + assert_eq!(p.next_arg().unwrap(), Short('y')); assert_eq!(p.next_arg(), None); } From 46af6eabb4f02f6deaf5386bf7f21568e2069d02 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 6 Mar 2025 13:12:41 -0600 Subject: [PATCH 10/35] fix(lexarg)!: Always allow = as a short flag From lexopt > If we find "-=[...]" we interpret it as an option. > If we find "-o=..." then there's an unexpected value. > ('-=' as an option exists, see https://linux.die.net/man/1/a2ps.) > clap always interprets it as a short flag in this case, but > that feels sloppy. If `=` is a short in one position, it makes sense for it to be a short in another position, so we're "being sloppy". --- crates/lexarg/src/lib.rs | 58 ++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 35 deletions(-) diff --git a/crates/lexarg/src/lib.rs b/crates/lexarg/src/lib.rs index 7444b8d..b6f0936 100644 --- a/crates/lexarg/src/lib.rs +++ b/crates/lexarg/src/lib.rs @@ -4,6 +4,7 @@ //! further so it can be used for CLI plugin systems. //! //! ## Example +//! //! ```no_run //! struct Args { //! thing: String, @@ -115,6 +116,10 @@ impl<'a> Parser<'a> { /// Returns `None` if the command line has been exhausted. /// /// Returns [`Arg::Unexpected`] on failure + /// + /// Notes: + /// - `=` is always accepted as a [`Arg::Short('=')`]. If that isn't the case in your + /// application, you may want to special case the error for that. pub fn next_arg(&mut self) -> Option> { // Always reset self.was_attached = false; @@ -132,40 +137,20 @@ impl<'a> Parser<'a> { let unparsed = &valid[index..]; let mut char_indices = unparsed.char_indices(); if let Some((0, short)) = char_indices.next() { - if short == '=' { - let arg = self - .raw - .get(self.current) - .expect("`current` is valid if state is `Shorts`"); - // SAFETY: everything preceding `index` were a short flags, making them valid UTF-8 - let unexpected_index = if index == 1 { - 0 - } else if short == '=' { - index + 1 - } else { - index - }; - let unexpected = unsafe { ext::split_at(arg, unexpected_index) }.1; - - self.state = None; - self.current += 1; - Some(Arg::Unexpected(unexpected)) + if let Some((offset, _)) = char_indices.next() { + let next_index = index + offset; + // Might have more flags + self.state = Some(State::PendingShorts(valid, invalid, next_index)); } else { - if let Some((offset, _)) = char_indices.next() { - let next_index = index + offset; - // Might have more flags - self.state = Some(State::PendingShorts(valid, invalid, next_index)); + // No more flags + if invalid.is_empty() { + self.state = None; + self.current += 1; } else { - // No more flags - if invalid.is_empty() { - self.state = None; - self.current += 1; - } else { - self.state = Some(State::PendingValue(invalid)); - } + self.state = Some(State::PendingValue(invalid)); } - Some(Arg::Short(short)) } + Some(Arg::Short(short)) } else { debug_assert_ne!(invalid, ""); if index == 1 { @@ -243,7 +228,7 @@ impl<'a> Parser<'a> { /// `None` is returned if there is not another applicable flag value, including: /// - No more arguments are present /// - `--` was encountered, meaning all remaining arguments are positional - /// - Being called again when the first value was attached (e.g. `--hello=world`) + /// - Being called again when the first value was attached (`--flag=value`, `-Fvalue`, `-F=value`) pub fn next_flag_value(&mut self) -> Option<&'a OsStr> { if self.was_attached { debug_assert!(!self.has_pending()); @@ -671,7 +656,8 @@ mod tests { let mut p = Parser::new(&["-a=b"]); assert_eq!(p.next_arg().unwrap(), Short('a')); - assert_eq!(p.next_arg().unwrap(), Unexpected(OsStr::new("b"))); + assert_eq!(p.next_arg().unwrap(), Short('=')); + assert_eq!(p.next_arg().unwrap(), Short('b')); assert_eq!(p.next_arg(), None); let mut p = Parser::new(&["-a="]); @@ -681,15 +667,16 @@ mod tests { let mut p = Parser::new(&["-a="]); assert_eq!(p.next_arg().unwrap(), Short('a')); - assert_eq!(p.next_arg().unwrap(), Unexpected(OsStr::new(""))); + assert_eq!(p.next_arg().unwrap(), Short('=')); assert_eq!(p.next_arg(), None); let mut p = Parser::new(&["-="]); - assert_eq!(p.next_arg().unwrap(), Unexpected(OsStr::new("-="))); + assert_eq!(p.next_arg().unwrap(), Short('=')); assert_eq!(p.next_arg(), None); let mut p = Parser::new(&["-=a"]); - assert_eq!(p.next_arg().unwrap(), Unexpected(OsStr::new("-=a"))); + assert_eq!(p.next_arg().unwrap(), Short('=')); + assert_eq!(p.next_arg().unwrap(), Short('a')); assert_eq!(p.next_arg(), None); } @@ -705,6 +692,7 @@ mod tests { let mut p = Parser::new(&args); assert_eq!(p.next_arg().unwrap(), Short('a')); + assert_eq!(p.next_arg().unwrap(), Short('=')); assert_eq!(p.next_arg().unwrap(), Unexpected(&bad)); assert_eq!(p.next_arg(), None); } From e25397d9f5dff30dd83dd607ef650c92f51bef70 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 6 Mar 2025 13:37:04 -0600 Subject: [PATCH 11/35] fix(lexarg): Ensure Escape is reported after an option --- crates/lexarg/src/lib.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/crates/lexarg/src/lib.rs b/crates/lexarg/src/lib.rs index b6f0936..0fd81e4 100644 --- a/crates/lexarg/src/lib.rs +++ b/crates/lexarg/src/lib.rs @@ -277,13 +277,10 @@ impl<'a> Parser<'a> { return None; } - let next = self.next_raw_()?; - - if next == "--" { - self.state = Some(State::Escaped); + if self.peek_raw()? == "--" { None } else { - Some(next) + self.next_raw_() } } @@ -300,6 +297,10 @@ impl<'a> Parser<'a> { } } + fn peek_raw(&self) -> Option<&'a OsStr> { + self.raw.get(self.current) + } + fn next_raw_(&mut self) -> Option<&'a OsStr> { debug_assert!(!self.has_pending()); debug_assert!(!self.was_attached); @@ -510,6 +511,7 @@ mod tests { let mut p = Parser::new(&["-x", "--", "-y"]); assert_eq!(p.next_arg().unwrap(), Short('x')); assert_eq!(p.next_flag_value(), None); + assert_eq!(p.next_arg().unwrap(), Escape); assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-y"))); assert_eq!(p.next_arg(), None); @@ -839,7 +841,10 @@ mod tests { matches!(parser.state, None | Some(State::Escaped)), "{next:?} via {path:?}", ); - if parser.state.is_none() && !parser.was_attached { + if parser.state.is_none() + && !parser.was_attached + && parser.peek_raw() != Some(OsStr::new("--")) + { assert_eq!(parser.current, parser.raw.len(), "{next:?} via {path:?}",); } } From 2fe9162d94009a79d6b6aef8c338755fd631656d Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 6 Mar 2025 13:38:27 -0600 Subject: [PATCH 12/35] docs(design): Clarify another lexarg design decision --- DESIGN.md | 1 + 1 file changed, 1 insertion(+) diff --git a/DESIGN.md b/DESIGN.md index a251d8a..14ba5c3 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -132,5 +132,6 @@ 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 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 From 57d10d10dc2f229757358882bff7ba956675adff Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 6 Mar 2025 14:30:19 -0600 Subject: [PATCH 13/35] refactor(lexarg): Remove extraneous state change --- crates/lexarg/src/lib.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/lexarg/src/lib.rs b/crates/lexarg/src/lib.rs index 0fd81e4..f3aebac 100644 --- a/crates/lexarg/src/lib.rs +++ b/crates/lexarg/src/lib.rs @@ -264,10 +264,7 @@ impl<'a> Parser<'a> { Some(remainder) } } - State::Escaped => { - self.state = Some(State::Escaped); - None - } + State::Escaped => None, } } From 43731fbb82e6ed4f4283e31c14640c2f34d57e02 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 6 Mar 2025 14:33:35 -0600 Subject: [PATCH 14/35] test(lexarg): Show bug with attached values --- crates/lexarg/src/lib.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/crates/lexarg/src/lib.rs b/crates/lexarg/src/lib.rs index f3aebac..23d0c4f 100644 --- a/crates/lexarg/src/lib.rs +++ b/crates/lexarg/src/lib.rs @@ -664,6 +664,23 @@ mod tests { assert_eq!(p.next_flag_value().unwrap(), OsStr::new("")); assert_eq!(p.next_arg(), None); + let mut p = Parser::new(&["-a=="]); + assert_eq!(p.next_arg().unwrap(), Short('a')); + assert_eq!(p.next_flag_value().unwrap(), OsStr::new("=")); + assert_eq!(p.next_arg(), None); + + let mut p = Parser::new(&["-abc=de"]); + assert_eq!(p.next_arg().unwrap(), Short('a')); + assert_eq!(p.next_flag_value().unwrap(), OsStr::new("de")); + assert_eq!(p.next_arg(), None); + + let mut p = Parser::new(&["-abc==de"]); + assert_eq!(p.next_arg().unwrap(), Short('a')); + assert_eq!(p.next_arg().unwrap(), Short('b')); + assert_eq!(p.next_arg().unwrap(), Short('c')); + assert_eq!(p.next_flag_value().unwrap(), OsStr::new("=de")); + assert_eq!(p.next_arg(), None); + let mut p = Parser::new(&["-a="]); assert_eq!(p.next_arg().unwrap(), Short('a')); assert_eq!(p.next_arg().unwrap(), Short('=')); From 2f08cf5cd3cdfb14bb1da4f63dd642e5c21b7cba Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 6 Mar 2025 14:38:33 -0600 Subject: [PATCH 15/35] fix(lexarg): Don't skip ahead to = sign --- crates/lexarg/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/lexarg/src/lib.rs b/crates/lexarg/src/lib.rs index 23d0c4f..6b2feb2 100644 --- a/crates/lexarg/src/lib.rs +++ b/crates/lexarg/src/lib.rs @@ -260,7 +260,7 @@ impl<'a> Parser<'a> { } else { // SAFETY: everything preceding `index` were a short flags, making them valid UTF-8 let remainder = unsafe { ext::split_at(arg, index) }.1; - let remainder = remainder.split_once("=").map(|s| s.1).unwrap_or(remainder); + let remainder = remainder.strip_prefix("=").unwrap_or(remainder); Some(remainder) } } @@ -671,7 +671,7 @@ mod tests { let mut p = Parser::new(&["-abc=de"]); assert_eq!(p.next_arg().unwrap(), Short('a')); - assert_eq!(p.next_flag_value().unwrap(), OsStr::new("de")); + assert_eq!(p.next_flag_value().unwrap(), OsStr::new("bc=de")); assert_eq!(p.next_arg(), None); let mut p = Parser::new(&["-abc==de"]); From b3866e4d5ddb97e9659ae466fe2a62d6694eab26 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 6 Mar 2025 14:42:39 -0600 Subject: [PATCH 16/35] refactor(lexarg); Ensure next_attached_value is standalone --- crates/lexarg/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/lexarg/src/lib.rs b/crates/lexarg/src/lib.rs index 6b2feb2..5535385 100644 --- a/crates/lexarg/src/lib.rs +++ b/crates/lexarg/src/lib.rs @@ -234,7 +234,6 @@ impl<'a> Parser<'a> { debug_assert!(!self.has_pending()); None } else if let Some(value) = self.next_attached_value() { - self.was_attached = true; Some(value) } else { self.next_detached_value() @@ -246,6 +245,7 @@ impl<'a> Parser<'a> { State::PendingValue(attached) => { self.state = None; self.current += 1; + self.was_attached = true; Some(attached) } State::PendingShorts(_, _, index) => { @@ -261,6 +261,7 @@ impl<'a> Parser<'a> { // SAFETY: everything preceding `index` were a short flags, making them valid UTF-8 let remainder = unsafe { ext::split_at(arg, index) }.1; let remainder = remainder.strip_prefix("=").unwrap_or(remainder); + self.was_attached = true; Some(remainder) } } From e1fa2430ec850c9de73be2188121fb6c9b80873c Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 7 Mar 2025 16:00:37 -0600 Subject: [PATCH 17/35] feat(lexarg): Expose Parser::next_attached_value --- crates/lexarg/src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/lexarg/src/lib.rs b/crates/lexarg/src/lib.rs index 5535385..b95f42e 100644 --- a/crates/lexarg/src/lib.rs +++ b/crates/lexarg/src/lib.rs @@ -240,7 +240,11 @@ impl<'a> Parser<'a> { } } - fn next_attached_value(&mut self) -> Option<&'a OsStr> { + /// Get a flag's attached value (`--flag=value`, `-Fvalue`, `-F=value`) + /// + /// This is a more specialized variant of [`Parser::next_flag_value`] for when only attached + /// values are allowed, e.g. `--color[=]`. + pub fn next_attached_value(&mut self) -> Option<&'a OsStr> { match self.state? { State::PendingValue(attached) => { self.state = None; From a0c042705614f17d3a1ebff1b94a52f5021a867a Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 7 Mar 2025 16:00:47 -0600 Subject: [PATCH 18/35] feat(lexarg): Expose Parser::remaining_raw --- crates/lexarg/src/lib.rs | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/crates/lexarg/src/lib.rs b/crates/lexarg/src/lib.rs index b95f42e..d32d2df 100644 --- a/crates/lexarg/src/lib.rs +++ b/crates/lexarg/src/lib.rs @@ -66,6 +66,7 @@ //! ``` #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![allow(clippy::result_unit_err)] #![warn(missing_debug_implementations)] #![warn(missing_docs)] #![warn(clippy::print_stderr)] @@ -289,7 +290,6 @@ impl<'a> Parser<'a> { /// Get the next argument, independent of what it looks like /// /// Returns `Err(())` if an [attached value][Parser::next_attached_value] is present - #[allow(clippy::result_unit_err)] pub fn next_raw(&mut self) -> Result, ()> { if self.has_pending() { Err(()) @@ -299,6 +299,18 @@ impl<'a> Parser<'a> { } } + /// Collect all remaining arguments, independent of what they look like + /// + /// Returns `Err(())` if an [attached value][Parser::next_attached_value] is present + pub fn remaining_raw(&mut self) -> Result + '_, ()> { + if self.has_pending() { + Err(()) + } else { + self.was_attached = false; + Ok(std::iter::from_fn(|| self.next_raw_())) + } + } + fn peek_raw(&self) -> Option<&'a OsStr> { self.raw.get(self.current) } @@ -718,6 +730,29 @@ mod tests { assert_eq!(p.next_arg(), None); } + #[test] + fn remaining_raw() { + let mut p = Parser::new(&["-a", "b", "c", "d"]); + assert_eq!( + p.remaining_raw().unwrap().collect::>(), + &["-a", "b", "c", "d"] + ); + // Consumed all + assert!(p.next_arg().is_none()); + assert!(p.remaining_raw().is_ok()); + assert_eq!(p.remaining_raw().unwrap().collect::>().len(), 0); + + let mut p = Parser::new(&["-ab", "c", "d"]); + p.next_arg().unwrap(); + // Attached value + assert!(p.remaining_raw().is_err()); + p.next_attached_value().unwrap(); + assert_eq!(p.remaining_raw().unwrap().collect::>(), &["c", "d"]); + // Consumed all + assert!(p.next_arg().is_none()); + assert_eq!(p.remaining_raw().unwrap().collect::>().len(), 0); + } + /// Transform @ characters into invalid unicode. fn bad_string(text: &str) -> std::ffi::OsString { #[cfg(any(unix, target_os = "wasi"))] From 6ab1334a19926edf91d4be6a5176f4d2faa26616 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 24 Mar 2025 16:06:45 -0500 Subject: [PATCH 19/35] refactor(lexarg): Clarify the error being specialized on index --- crates/lexarg/src/lib.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/lexarg/src/lib.rs b/crates/lexarg/src/lib.rs index d32d2df..a50cbe5 100644 --- a/crates/lexarg/src/lib.rs +++ b/crates/lexarg/src/lib.rs @@ -154,7 +154,10 @@ impl<'a> Parser<'a> { Some(Arg::Short(short)) } else { debug_assert_ne!(invalid, ""); - if index == 1 { + if index == 0 { + panic!("there should have been a `-`") + } else if index == 1 { + // Like long flags, include `-` let arg = self .raw .get(self.current) From 30d57579b8e83f50e82fd6c44e8e858783ec963c Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 24 Mar 2025 16:22:33 -0500 Subject: [PATCH 20/35] fix(lexarg)!: Switch to `Short(str)` --- DESIGN.md | 12 +++ crates/lexarg-error/src/lib.rs | 2 +- crates/lexarg/src/lib.rs | 111 ++++++++++++------------- crates/libtest-lexarg/src/lib.rs | 4 +- crates/libtest2-harness/src/harness.rs | 2 +- 5 files changed, 71 insertions(+), 60 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index 14ba5c3..102c58d 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -135,3 +135,15 @@ In reviewing lexopt's API: - Escapes aren't explicitly communicated which makes communal parsing more difficult 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 + +### 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. diff --git a/crates/lexarg-error/src/lib.rs b/crates/lexarg-error/src/lib.rs index bed1699..da7d298 100644 --- a/crates/lexarg-error/src/lib.rs +++ b/crates/lexarg-error/src/lib.rs @@ -25,7 +25,7 @@ //! let _bin_name = parser.next_raw(); //! while let Some(arg) = parser.next_arg() { //! match arg { -//! Short('n') | Long("number") => { +//! Short("n") | Long("number") => { //! number = parser //! .next_flag_value().ok_or_else(|| Error::msg("`--number` requires a value"))? //! .to_str().ok_or_else(|| Error::msg("invalid number"))? diff --git a/crates/lexarg/src/lib.rs b/crates/lexarg/src/lib.rs index a50cbe5..80f44ee 100644 --- a/crates/lexarg/src/lib.rs +++ b/crates/lexarg/src/lib.rs @@ -23,7 +23,7 @@ //! let _bin_name = parser.next_raw(); //! while let Some(arg) = parser.next_arg() { //! match arg { -//! Short('n') | Long("number") => { +//! Short("n") | Long("number") => { //! number = parser //! .next_flag_value().ok_or("`--number` requires a value")? //! .to_str().ok_or("invalid number")? @@ -119,7 +119,7 @@ impl<'a> Parser<'a> { /// Returns [`Arg::Unexpected`] on failure /// /// Notes: - /// - `=` is always accepted as a [`Arg::Short('=')`]. If that isn't the case in your + /// - `=` is always accepted as a [`Arg::Short("=")`]. If that isn't the case in your /// application, you may want to special case the error for that. pub fn next_arg(&mut self) -> Option> { // Always reset @@ -135,23 +135,18 @@ impl<'a> Parser<'a> { Some(State::PendingShorts(valid, invalid, index)) => { // We're somewhere inside a `-abc` chain. Because we're in `.next_arg()`, not `.next_flag_value()`, we // can assume that the next character is another option. - let unparsed = &valid[index..]; - let mut char_indices = unparsed.char_indices(); - if let Some((0, short)) = char_indices.next() { - if let Some((offset, _)) = char_indices.next() { - let next_index = index + offset; - // Might have more flags + if let Some(next_index) = ceil_char_boundary(valid, index) { + if next_index < valid.len() { self.state = Some(State::PendingShorts(valid, invalid, next_index)); + } else if !invalid.is_empty() { + self.state = Some(State::PendingValue(invalid)); } else { // No more flags - if invalid.is_empty() { - self.state = None; - self.current += 1; - } else { - self.state = Some(State::PendingValue(invalid)); - } + self.state = None; + self.current += 1; } - Some(Arg::Short(short)) + let flag = &valid[index..next_index]; + Some(Arg::Short(flag)) } else { debug_assert_ne!(invalid, ""); if index == 0 { @@ -434,8 +429,8 @@ impl State<'_> { /// A command line argument found by [`Parser`], either an option or a positional argument #[derive(Debug, Clone, PartialEq, Eq)] pub enum Arg<'a> { - /// A short option, e.g. `Short('q')` for `-q` - Short(char), + /// A short option, e.g. `Short("q")` for `-q` + Short(&'a str), /// A long option, e.g. `Long("verbose")` for `--verbose` /// /// The dashes are not included @@ -460,6 +455,10 @@ fn split_nonutf8_once(b: &OsStr) -> (&str, Option<&OsStr>) { } } +fn ceil_char_boundary(s: &str, curr_boundary: usize) -> Option { + (curr_boundary + 1..=s.len()).find(|i| s.is_char_boundary(*i)) +} + mod private { use super::OsStr; @@ -477,7 +476,7 @@ mod tests { #[test] fn test_basic() { let mut p = Parser::new(&["-n", "10", "foo", "-", "--", "baz", "-qux"]); - assert_eq!(p.next_arg().unwrap(), Short('n')); + assert_eq!(p.next_arg().unwrap(), Short("n")); assert_eq!(p.next_flag_value().unwrap(), "10"); assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("foo"))); assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-"))); @@ -492,13 +491,13 @@ mod tests { #[test] fn test_combined() { let mut p = Parser::new(&["-abc", "-fvalue", "-xfvalue"]); - assert_eq!(p.next_arg().unwrap(), Short('a')); - assert_eq!(p.next_arg().unwrap(), Short('b')); - assert_eq!(p.next_arg().unwrap(), Short('c')); - assert_eq!(p.next_arg().unwrap(), Short('f')); + assert_eq!(p.next_arg().unwrap(), Short("a")); + assert_eq!(p.next_arg().unwrap(), Short("b")); + assert_eq!(p.next_arg().unwrap(), Short("c")); + assert_eq!(p.next_arg().unwrap(), Short("f")); assert_eq!(p.next_flag_value().unwrap(), "value"); - assert_eq!(p.next_arg().unwrap(), Short('x')); - assert_eq!(p.next_arg().unwrap(), Short('f')); + assert_eq!(p.next_arg().unwrap(), Short("x")); + assert_eq!(p.next_arg().unwrap(), Short("f")); assert_eq!(p.next_flag_value().unwrap(), "value"); assert_eq!(p.next_arg(), None); } @@ -519,14 +518,14 @@ mod tests { fn test_dash_args() { // "--" should indicate the end of the options let mut p = Parser::new(&["-x", "--", "-y"]); - assert_eq!(p.next_arg().unwrap(), Short('x')); + assert_eq!(p.next_arg().unwrap(), Short("x")); assert_eq!(p.next_arg().unwrap(), Escape); assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-y"))); assert_eq!(p.next_arg(), None); // ...even if it's an argument of an option let mut p = Parser::new(&["-x", "--", "-y"]); - assert_eq!(p.next_arg().unwrap(), Short('x')); + assert_eq!(p.next_arg().unwrap(), Short("x")); assert_eq!(p.next_flag_value(), None); assert_eq!(p.next_arg().unwrap(), Escape); assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-y"))); @@ -534,24 +533,24 @@ mod tests { // "-" is a valid value that should not be treated as an option let mut p = Parser::new(&["-x", "-", "-y"]); - assert_eq!(p.next_arg().unwrap(), Short('x')); + assert_eq!(p.next_arg().unwrap(), Short("x")); assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-"))); - assert_eq!(p.next_arg().unwrap(), Short('y')); + assert_eq!(p.next_arg().unwrap(), Short("y")); assert_eq!(p.next_arg(), None); // '-' is a silly and hard to use short option, but other parsers treat // it like an option in this position let mut p = Parser::new(&["-x-y"]); - assert_eq!(p.next_arg().unwrap(), Short('x')); - assert_eq!(p.next_arg().unwrap(), Short('-')); - assert_eq!(p.next_arg().unwrap(), Short('y')); + assert_eq!(p.next_arg().unwrap(), Short("x")); + assert_eq!(p.next_arg().unwrap(), Short("-")); + assert_eq!(p.next_arg().unwrap(), Short("y")); assert_eq!(p.next_arg(), None); } #[test] fn test_missing_value() { let mut p = Parser::new(&["-o"]); - assert_eq!(p.next_arg().unwrap(), Short('o')); + assert_eq!(p.next_arg().unwrap(), Short("o")); assert_eq!(p.next_flag_value(), None); let mut q = Parser::new(&["--out"]); @@ -571,7 +570,7 @@ mod tests { assert_eq!(p.next_arg().unwrap(), Unexpected(OsStr::new("--="))); assert_eq!(p.next_arg().unwrap(), Unexpected(OsStr::new("--=3"))); assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-"))); - assert_eq!(p.next_arg().unwrap(), Short('x')); + assert_eq!(p.next_arg().unwrap(), Short("x")); assert_eq!(p.next_arg().unwrap(), Escape); assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-"))); assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-x"))); @@ -593,8 +592,8 @@ mod tests { #[test] fn test_unicode() { let mut p = Parser::new(&["-aµ", "--µ=10", "µ", "--foo=µ"]); - assert_eq!(p.next_arg().unwrap(), Short('a')); - assert_eq!(p.next_arg().unwrap(), Short('µ')); + assert_eq!(p.next_arg().unwrap(), Short("a")); + assert_eq!(p.next_arg().unwrap(), Short("µ")); assert_eq!(p.next_arg().unwrap(), Long("µ")); assert_eq!(p.next_flag_value().unwrap(), "10"); assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("µ"))); @@ -612,12 +611,12 @@ mod tests { let args = [bad_string("-💣@@@")]; let mut q = Parser::new(&args); - assert_eq!(q.next_arg().unwrap(), Short('💣')); + assert_eq!(q.next_arg().unwrap(), Short("💣")); assert_eq!(q.next_flag_value().unwrap(), bad_string("@@@")); let args = [bad_string("-f@@@")]; let mut r = Parser::new(&args); - assert_eq!(r.next_arg().unwrap(), Short('f')); + assert_eq!(r.next_arg().unwrap(), Short("f")); assert_eq!(r.next_arg().unwrap(), Unexpected(&bad_string("@@@"))); assert_eq!(r.next_arg(), None); @@ -662,57 +661,57 @@ mod tests { #[test] fn short_opt_equals_sign() { let mut p = Parser::new(&["-a=b"]); - assert_eq!(p.next_arg().unwrap(), Short('a')); + assert_eq!(p.next_arg().unwrap(), Short("a")); assert_eq!(p.next_flag_value().unwrap(), OsStr::new("b")); assert_eq!(p.next_arg(), None); let mut p = Parser::new(&["-a=b", "c"]); - assert_eq!(p.next_arg().unwrap(), Short('a')); + assert_eq!(p.next_arg().unwrap(), Short("a")); assert_eq!(p.next_flag_value().unwrap(), OsStr::new("b")); assert_eq!(p.next_flag_value(), None); assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("c"))); assert_eq!(p.next_arg(), None); let mut p = Parser::new(&["-a=b"]); - assert_eq!(p.next_arg().unwrap(), Short('a')); - assert_eq!(p.next_arg().unwrap(), Short('=')); - assert_eq!(p.next_arg().unwrap(), Short('b')); + assert_eq!(p.next_arg().unwrap(), Short("a")); + assert_eq!(p.next_arg().unwrap(), Short("=")); + assert_eq!(p.next_arg().unwrap(), Short("b")); assert_eq!(p.next_arg(), None); let mut p = Parser::new(&["-a="]); - assert_eq!(p.next_arg().unwrap(), Short('a')); + assert_eq!(p.next_arg().unwrap(), Short("a")); assert_eq!(p.next_flag_value().unwrap(), OsStr::new("")); assert_eq!(p.next_arg(), None); let mut p = Parser::new(&["-a=="]); - assert_eq!(p.next_arg().unwrap(), Short('a')); + assert_eq!(p.next_arg().unwrap(), Short("a")); assert_eq!(p.next_flag_value().unwrap(), OsStr::new("=")); assert_eq!(p.next_arg(), None); let mut p = Parser::new(&["-abc=de"]); - assert_eq!(p.next_arg().unwrap(), Short('a')); + assert_eq!(p.next_arg().unwrap(), Short("a")); assert_eq!(p.next_flag_value().unwrap(), OsStr::new("bc=de")); assert_eq!(p.next_arg(), None); let mut p = Parser::new(&["-abc==de"]); - assert_eq!(p.next_arg().unwrap(), Short('a')); - assert_eq!(p.next_arg().unwrap(), Short('b')); - assert_eq!(p.next_arg().unwrap(), Short('c')); + assert_eq!(p.next_arg().unwrap(), Short("a")); + assert_eq!(p.next_arg().unwrap(), Short("b")); + assert_eq!(p.next_arg().unwrap(), Short("c")); assert_eq!(p.next_flag_value().unwrap(), OsStr::new("=de")); assert_eq!(p.next_arg(), None); let mut p = Parser::new(&["-a="]); - assert_eq!(p.next_arg().unwrap(), Short('a')); - assert_eq!(p.next_arg().unwrap(), Short('=')); + assert_eq!(p.next_arg().unwrap(), Short("a")); + assert_eq!(p.next_arg().unwrap(), Short("=")); assert_eq!(p.next_arg(), None); let mut p = Parser::new(&["-="]); - assert_eq!(p.next_arg().unwrap(), Short('=')); + assert_eq!(p.next_arg().unwrap(), Short("=")); assert_eq!(p.next_arg(), None); let mut p = Parser::new(&["-=a"]); - assert_eq!(p.next_arg().unwrap(), Short('=')); - assert_eq!(p.next_arg().unwrap(), Short('a')); + assert_eq!(p.next_arg().unwrap(), Short("=")); + assert_eq!(p.next_arg().unwrap(), Short("a")); assert_eq!(p.next_arg(), None); } @@ -722,13 +721,13 @@ mod tests { let bad = bad_string("@"); let args = [bad_string("-a=@")]; let mut p = Parser::new(&args); - assert_eq!(p.next_arg().unwrap(), Short('a')); + assert_eq!(p.next_arg().unwrap(), Short("a")); assert_eq!(p.next_flag_value().unwrap(), bad_string("@")); assert_eq!(p.next_arg(), None); let mut p = Parser::new(&args); - assert_eq!(p.next_arg().unwrap(), Short('a')); - assert_eq!(p.next_arg().unwrap(), Short('=')); + assert_eq!(p.next_arg().unwrap(), Short("a")); + assert_eq!(p.next_arg().unwrap(), Short("=")); assert_eq!(p.next_arg().unwrap(), Unexpected(&bad)); assert_eq!(p.next_arg(), None); } diff --git a/crates/libtest-lexarg/src/lib.rs b/crates/libtest-lexarg/src/lib.rs index f4c0c67..150487e 100644 --- a/crates/libtest-lexarg/src/lib.rs +++ b/crates/libtest-lexarg/src/lib.rs @@ -376,7 +376,7 @@ impl TestOptsParseState { } }; } - Arg::Short('q') | Arg::Long("quiet") => { + Arg::Short("q") | Arg::Long("quiet") => { self.format = None; self.quiet = true; } @@ -406,7 +406,7 @@ impl TestOptsParseState { Arg::Long("show-output") => { self.opts.options.display_output = true; } - Arg::Short('Z') => { + Arg::Short("Z") => { let feature = parser .next_flag_value() .ok_or_else(|| Error::msg("`-Z` requires a feature name"))? diff --git a/crates/libtest2-harness/src/harness.rs b/crates/libtest2-harness/src/harness.rs index ffa6e8a..d6383b8 100644 --- a/crates/libtest2-harness/src/harness.rs +++ b/crates/libtest2-harness/src/harness.rs @@ -76,7 +76,7 @@ fn parse(parser: &mut cli::Parser<'_>) -> cli::Result let bin = parser.next_raw().expect("first arg, no pending values"); while let Some(arg) = parser.next_arg() { match arg { - cli::Arg::Short('h') | cli::Arg::Long("help") => { + cli::Arg::Short("h") | cli::Arg::Long("help") => { let bin = bin .unwrap_or_else(|| std::ffi::OsStr::new("test")) .to_string_lossy(); From a2740bebd3f77ce8859633c67c5122eb6d181a85 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 24 Mar 2025 16:26:47 -0500 Subject: [PATCH 21/35] fix(lexarg)!: Switch to `Escape(str)` This provides span information and allows users to do more custom logic with this. --- crates/lexarg/src/lib.rs | 12 ++++++------ crates/libtest-lexarg/src/lib.rs | 2 +- crates/libtest2-harness/src/harness.rs | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/lexarg/src/lib.rs b/crates/lexarg/src/lib.rs index 80f44ee..4a06de2 100644 --- a/crates/lexarg/src/lib.rs +++ b/crates/lexarg/src/lib.rs @@ -176,7 +176,7 @@ impl<'a> Parser<'a> { if arg == "--" { self.state = Some(State::Escaped); self.current += 1; - Some(Arg::Escape) + Some(Arg::Escape(arg.to_str().expect("`--` is valid UTF-8"))) } else if arg == "-" { self.state = None; self.current += 1; @@ -438,7 +438,7 @@ pub enum Arg<'a> { /// A positional argument, e.g. `/dev/null` Value(&'a OsStr), /// Marks the following values have been escaped with `--` - Escape, + Escape(&'a str), /// User passed something in that doesn't work Unexpected(&'a OsStr), } @@ -480,7 +480,7 @@ mod tests { assert_eq!(p.next_flag_value().unwrap(), "10"); assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("foo"))); assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-"))); - assert_eq!(p.next_arg().unwrap(), Escape); + assert_eq!(p.next_arg().unwrap(), Escape("--")); assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("baz"))); assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-qux"))); assert_eq!(p.next_arg(), None); @@ -519,7 +519,7 @@ mod tests { // "--" should indicate the end of the options let mut p = Parser::new(&["-x", "--", "-y"]); assert_eq!(p.next_arg().unwrap(), Short("x")); - assert_eq!(p.next_arg().unwrap(), Escape); + assert_eq!(p.next_arg().unwrap(), Escape("--")); assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-y"))); assert_eq!(p.next_arg(), None); @@ -527,7 +527,7 @@ mod tests { let mut p = Parser::new(&["-x", "--", "-y"]); assert_eq!(p.next_arg().unwrap(), Short("x")); assert_eq!(p.next_flag_value(), None); - assert_eq!(p.next_arg().unwrap(), Escape); + assert_eq!(p.next_arg().unwrap(), Escape("--")); assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-y"))); assert_eq!(p.next_arg(), None); @@ -571,7 +571,7 @@ mod tests { assert_eq!(p.next_arg().unwrap(), Unexpected(OsStr::new("--=3"))); assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-"))); assert_eq!(p.next_arg().unwrap(), Short("x")); - assert_eq!(p.next_arg().unwrap(), Escape); + assert_eq!(p.next_arg().unwrap(), Escape("--")); assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-"))); assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("-x"))); assert_eq!(p.next_arg().unwrap(), Value(OsStr::new("--"))); diff --git a/crates/libtest-lexarg/src/lib.rs b/crates/libtest-lexarg/src/lib.rs index 150487e..5591f82 100644 --- a/crates/libtest-lexarg/src/lib.rs +++ b/crates/libtest-lexarg/src/lib.rs @@ -449,7 +449,7 @@ impl TestOptsParseState { self.opts.shuffle_seed = Some(seed); } // All values are the same, whether escaped or not, so its a no-op - Arg::Escape => {} + Arg::Escape(_) => {} Arg::Value(filter) => { let filter = filter .to_str() diff --git a/crates/libtest2-harness/src/harness.rs b/crates/libtest2-harness/src/harness.rs index d6383b8..d84527d 100644 --- a/crates/libtest2-harness/src/harness.rs +++ b/crates/libtest2-harness/src/harness.rs @@ -104,7 +104,7 @@ fn parse(parser: &mut cli::Parser<'_>) -> cli::Result cli::Arg::Long(v) => { format!("unrecognized `--{v}` flag") } - cli::Arg::Escape => "handled `--`".to_owned(), + cli::Arg::Escape(_) => "handled `--`".to_owned(), cli::Arg::Value(v) => { format!("unrecognized `{}` value", v.to_string_lossy()) } From aba8d477efe76ff8377c2808070de2ecc650d8be Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 25 Mar 2025 11:46:07 -0500 Subject: [PATCH 22/35] feat(lexarg): Add Parser::peek_raw --- crates/lexarg/src/lib.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/lexarg/src/lib.rs b/crates/lexarg/src/lib.rs index 4a06de2..93ddbf3 100644 --- a/crates/lexarg/src/lib.rs +++ b/crates/lexarg/src/lib.rs @@ -278,7 +278,7 @@ impl<'a> Parser<'a> { return None; } - if self.peek_raw()? == "--" { + if self.peek_raw_()? == "--" { None } else { self.next_raw_() @@ -309,7 +309,18 @@ impl<'a> Parser<'a> { } } - fn peek_raw(&self) -> Option<&'a OsStr> { + /// Get the next argument, independent of what it looks like + /// + /// Returns `Err(())` if an [attached value][Parser::next_attached_value] is present + pub fn peek_raw(&self) -> Result, ()> { + if self.has_pending() { + Err(()) + } else { + Ok(self.peek_raw_()) + } + } + + fn peek_raw_(&self) -> Option<&'a OsStr> { self.raw.get(self.current) } @@ -899,7 +910,7 @@ mod tests { ); if parser.state.is_none() && !parser.was_attached - && parser.peek_raw() != Some(OsStr::new("--")) + && parser.peek_raw_() != Some(OsStr::new("--")) { assert_eq!(parser.current, parser.raw.len(), "{next:?} via {path:?}",); } From 3646515fe5725ac47bf46242e64a39b15d02e9ef Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 25 Mar 2025 14:53:24 -0500 Subject: [PATCH 23/35] feat(lexarg): Make Arg copyable --- crates/lexarg/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/lexarg/src/lib.rs b/crates/lexarg/src/lib.rs index 93ddbf3..175574c 100644 --- a/crates/lexarg/src/lib.rs +++ b/crates/lexarg/src/lib.rs @@ -438,7 +438,7 @@ impl State<'_> { } /// A command line argument found by [`Parser`], either an option or a positional argument -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum Arg<'a> { /// A short option, e.g. `Short("q")` for `-q` Short(&'a str), From 4dd2845da8a70fdc4a7b1800f05e1cf9af72905a Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 25 Mar 2025 14:32:08 -0500 Subject: [PATCH 24/35] docs(lexarg): Remove bad example --- crates/lexarg-error/src/lib.rs | 46 +--------------------------------- 1 file changed, 1 insertion(+), 45 deletions(-) diff --git a/crates/lexarg-error/src/lib.rs b/crates/lexarg-error/src/lib.rs index da7d298..740f9fb 100644 --- a/crates/lexarg-error/src/lib.rs +++ b/crates/lexarg-error/src/lib.rs @@ -77,51 +77,7 @@ #[cfg(doctest)] pub struct ReadmeDoctests; -/// `Result` -/// -/// `lexarg_error::Result` may be used with one *or* two type parameters. -/// -/// ```rust -/// use lexarg_error::Result; -/// -/// # const IGNORE: &str = stringify! { -/// fn demo1() -> Result {...} -/// // ^ equivalent to std::result::Result -/// -/// fn demo2() -> Result {...} -/// // ^ equivalent to std::result::Result -/// # }; -/// ``` -/// -/// # Example -/// -/// ``` -/// # pub trait Deserialize {} -/// # -/// # mod serde_json { -/// # use super::Deserialize; -/// # use std::io; -/// # -/// # pub fn from_str(json: &str) -> io::Result { -/// # 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 = std::result::Result; /// Argument error type for use with lexarg From a59ff1d419c8fea4ac1d673c2b56430b2a9ceef0 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 25 Mar 2025 14:41:17 -0500 Subject: [PATCH 25/35] docs(lexarg): Clarify docs intro --- crates/lexarg-error/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/lexarg-error/src/lib.rs b/crates/lexarg-error/src/lib.rs index 740f9fb..346c4c0 100644 --- a/crates/lexarg-error/src/lib.rs +++ b/crates/lexarg-error/src/lib.rs @@ -1,4 +1,4 @@ -//! 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. From 55096dc658d6c5b239437236eed55f73e7009015 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 25 Mar 2025 14:45:27 -0500 Subject: [PATCH 26/35] refactor(lexarg): Prefer Self over naming the type --- crates/lexarg-error/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/lexarg-error/src/lib.rs b/crates/lexarg-error/src/lib.rs index 346c4c0..72b88c7 100644 --- a/crates/lexarg-error/src/lib.rs +++ b/crates/lexarg-error/src/lib.rs @@ -93,7 +93,7 @@ impl Error { where M: std::fmt::Display, { - Error { + Self { msg: message.to_string(), } } @@ -105,7 +105,7 @@ where { #[cold] fn from(error: E) -> Self { - Error::msg(error) + Self::msg(error) } } From 8fde3b294104240839182ca992eff37610d32c85 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 25 Mar 2025 14:47:43 -0500 Subject: [PATCH 27/35] fix(lexarg)!: Pull out an ErrorContext builder --- crates/lexarg-error/src/lib.rs | 45 +++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/crates/lexarg-error/src/lib.rs b/crates/lexarg-error/src/lib.rs index 72b88c7..98f66df 100644 --- a/crates/lexarg-error/src/lib.rs +++ b/crates/lexarg-error/src/lib.rs @@ -6,6 +6,7 @@ //! ## Example //! ```no_run //! use lexarg_error::Error; +//! use lexarg_error::ErrorContext; //! use lexarg_error::Result; //! //! struct Args { @@ -27,15 +28,15 @@ //! match arg { //! Short("n") | Long("number") => { //! number = parser -//! .next_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))?; +//! .next_flag_value().ok_or_else(|| ErrorContext::msg("`--number` requires a value"))? +//! .to_str().ok_or_else(|| ErrorContext::msg("invalid number"))? +//! .parse().map_err(|e| ErrorContext::msg(e))?; //! } //! Long("shout") => { //! shout = true; //! } //! Value(val) if thing.is_none() => { -//! thing = Some(val.to_str().ok_or_else(|| Error::msg("invalid number"))?); +//! thing = Some(val.to_str().ok_or_else(|| ErrorContext::msg("invalid number"))?); //! } //! Long("help") => { //! println!("Usage: hello [-n|--number=NUM] [--shout] THING"); @@ -99,7 +100,39 @@ impl Error { } } -impl From for Error +impl From for Error { + #[cold] + fn from(error: ErrorContext) -> Self { + Self::msg(error.to_string()) + } +} + +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 { + msg: String, +} + +impl ErrorContext { + /// Create a new error object from a printable error message. + #[cold] + pub fn msg(message: M) -> Self + where + M: std::fmt::Display, + { + Self { + msg: message.to_string(), + } + } +} + +impl From for ErrorContext where E: std::error::Error + Send + Sync + 'static, { @@ -109,7 +142,7 @@ where } } -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) } From 0026ba88e9c0507c087867a62b6f9aba9650aa6a Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 25 Mar 2025 15:35:44 -0500 Subject: [PATCH 28/35] feat(lexarg): Allow reporting the argument the operation failed within --- crates/lexarg-error/Cargo.toml | 2 +- crates/lexarg-error/src/lib.rs | 56 +++++++++++++++++++++++++--------- 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/crates/lexarg-error/Cargo.toml b/crates/lexarg-error/Cargo.toml index 3614dea..77a1c43 100644 --- a/crates/lexarg-error/Cargo.toml +++ b/crates/lexarg-error/Cargo.toml @@ -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 diff --git a/crates/lexarg-error/src/lib.rs b/crates/lexarg-error/src/lib.rs index 98f66df..f1d6ae3 100644 --- a/crates/lexarg-error/src/lib.rs +++ b/crates/lexarg-error/src/lib.rs @@ -23,33 +23,39 @@ //! let mut shout = false; //! let mut raw = std::env::args_os().collect::>(); //! let mut parser = lexarg::Parser::new(&raw); -//! let _bin_name = parser.next_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") => { //! number = parser -//! .next_flag_value().ok_or_else(|| ErrorContext::msg("`--number` requires a value"))? -//! .to_str().ok_or_else(|| ErrorContext::msg("invalid number"))? -//! .parse().map_err(|e| ErrorContext::msg(e))?; +//! .next_flag_value().ok_or_else(|| ErrorContext::msg("missing required value").within(arg))? +//! .to_str().ok_or_else(|| ErrorContext::msg("invalid number").within(arg))? +//! .parse().map_err(|e| ErrorContext::msg(e).within(arg))?; //! } //! Long("shout") => { //! shout = true; //! } //! Value(val) if thing.is_none() => { -//! thing = Some(val.to_str().ok_or_else(|| ErrorContext::msg("invalid number"))?); +//! thing = Some(val +//! .to_str() +//! .ok_or_else(|| ErrorContext::msg("invalid number").within(arg))? +//! ); //! } //! Long("help") => { //! println!("Usage: hello [-n|--number=NUM] [--shout] THING"); //! std::process::exit(0); //! } //! _ => { -//! return Err(Error::msg("unexpected argument")); +//! return Err(ErrorContext::msg("unexpected argument").within(arg).into()); //! } //! } //! } //! //! Ok(Args { -//! thing: thing.ok_or_else(|| Error::msg("missing argument THING"))?.to_owned(), +//! thing: thing.ok_or_else(|| ErrorContext::msg("missing argument THING").within(lexarg::Arg::Value(bin_name)))?.to_owned(), //! number, //! shout, //! }) @@ -100,9 +106,9 @@ impl Error { } } -impl From for Error { +impl From> for Error { #[cold] - fn from(error: ErrorContext) -> Self { + fn from(error: ErrorContext<'_>) -> Self { Self::msg(error.to_string()) } } @@ -115,11 +121,12 @@ impl std::fmt::Display for Error { /// Collect context for creating an [`Error`] #[derive(Debug)] -pub struct ErrorContext { +pub struct ErrorContext<'a> { msg: String, + within: Option>, } -impl ErrorContext { +impl<'a> ErrorContext<'a> { /// Create a new error object from a printable error message. #[cold] pub fn msg(message: M) -> Self @@ -128,11 +135,19 @@ impl ErrorContext { { Self { msg: message.to_string(), + within: None, } } + + /// [`Arg`][lexarg::Arg] the error occurred within + #[cold] + pub fn within(mut self, within: lexarg::Arg<'a>) -> Self { + self.within = Some(within); + self + } } -impl From for ErrorContext +impl From for ErrorContext<'_> where E: std::error::Error + Send + Sync + 'static, { @@ -142,8 +157,21 @@ where } } -impl std::fmt::Display for ErrorContext { +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(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(()) } } From 7442bd53cb9f22dfc25ed54a7b3c89e280c79ed2 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 25 Mar 2025 15:39:15 -0500 Subject: [PATCH 29/35] docs(lexarg): Split up operation in example --- crates/lexarg-error/src/lib.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/lexarg-error/src/lib.rs b/crates/lexarg-error/src/lib.rs index f1d6ae3..0902fb0 100644 --- a/crates/lexarg-error/src/lib.rs +++ b/crates/lexarg-error/src/lib.rs @@ -30,8 +30,10 @@ //! while let Some(arg) = parser.next_arg() { //! match arg { //! Short("n") | Long("number") => { -//! number = parser -//! .next_flag_value().ok_or_else(|| ErrorContext::msg("missing required value").within(arg))? +//! 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").within(arg))? //! .parse().map_err(|e| ErrorContext::msg(e).within(arg))?; //! } From 19a1bdb14f2881971efa4d350843815624e85085 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 25 Mar 2025 15:53:06 -0500 Subject: [PATCH 30/35] feat(lexarg): Allow reporting the argument that failed --- crates/lexarg-error/src/lib.rs | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/crates/lexarg-error/src/lib.rs b/crates/lexarg-error/src/lib.rs index 0902fb0..c3ab829 100644 --- a/crates/lexarg-error/src/lib.rs +++ b/crates/lexarg-error/src/lib.rs @@ -34,8 +34,8 @@ //! .next_flag_value() //! .ok_or_else(|| ErrorContext::msg("missing required value").within(arg))?; //! number = value -//! .to_str().ok_or_else(|| ErrorContext::msg("invalid number").within(arg))? -//! .parse().map_err(|e| ErrorContext::msg(e).within(arg))?; +//! .to_str().ok_or_else(|| ErrorContext::msg("invalid number").unexpected(lexarg::Arg::Value(value)).within(arg))? +//! .parse().map_err(|e| ErrorContext::msg(e).unexpected(lexarg::Arg::Value(value)).within(arg))?; //! } //! Long("shout") => { //! shout = true; @@ -43,7 +43,7 @@ //! Value(val) if thing.is_none() => { //! thing = Some(val //! .to_str() -//! .ok_or_else(|| ErrorContext::msg("invalid number").within(arg))? +//! .ok_or_else(|| ErrorContext::msg("invalid number").unexpected(arg))? //! ); //! } //! Long("help") => { @@ -51,7 +51,7 @@ //! std::process::exit(0); //! } //! _ => { -//! return Err(ErrorContext::msg("unexpected argument").within(arg).into()); +//! return Err(ErrorContext::msg("unexpected argument").unexpected(arg).within(lexarg::Arg::Value(bin_name)).into()); //! } //! } //! } @@ -126,6 +126,7 @@ impl std::fmt::Display for Error { pub struct ErrorContext<'a> { msg: String, within: Option>, + unexpected: Option>, } impl<'a> ErrorContext<'a> { @@ -138,6 +139,7 @@ impl<'a> ErrorContext<'a> { Self { msg: message.to_string(), within: None, + unexpected: None, } } @@ -147,6 +149,13 @@ impl<'a> ErrorContext<'a> { 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 From for ErrorContext<'_> @@ -162,6 +171,18 @@ where impl std::fmt::Display for ErrorContext<'_> { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 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 { From 37dfadaf25c96a9604e9007b9501c8ddadf2eefe Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 25 Mar 2025 15:53:24 -0500 Subject: [PATCH 31/35] docs(lexarg): Fix a bad value in an example --- crates/lexarg-error/src/lib.rs | 2 +- crates/lexarg/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/lexarg-error/src/lib.rs b/crates/lexarg-error/src/lib.rs index c3ab829..e881371 100644 --- a/crates/lexarg-error/src/lib.rs +++ b/crates/lexarg-error/src/lib.rs @@ -43,7 +43,7 @@ //! Value(val) if thing.is_none() => { //! thing = Some(val //! .to_str() -//! .ok_or_else(|| ErrorContext::msg("invalid number").unexpected(arg))? +//! .ok_or_else(|| ErrorContext::msg("invalid string").unexpected(arg))? //! ); //! } //! Long("help") => { diff --git a/crates/lexarg/src/lib.rs b/crates/lexarg/src/lib.rs index 175574c..e369c33 100644 --- a/crates/lexarg/src/lib.rs +++ b/crates/lexarg/src/lib.rs @@ -33,7 +33,7 @@ //! shout = true; //! } //! Value(val) if thing.is_none() => { -//! thing = Some(val.to_str().ok_or("invalid number")?); +//! thing = Some(val.to_str().ok_or("invalid string")?); //! } //! Long("help") => { //! println!("Usage: hello [-n|--number=NUM] [--shout] THING"); From 313d30a648dfdea4ef2b728d6230d6f9099ebfe9 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 25 Mar 2025 16:01:07 -0500 Subject: [PATCH 32/35] docs(lexarg): Pull out doc examples into targets --- crates/lexarg-error/examples/hello-error.rs | 80 +++++++++++++++++++++ crates/lexarg-error/src/lib.rs | 71 +----------------- crates/lexarg/examples/hello.rs | 61 ++++++++++++++++ crates/lexarg/src/lib.rs | 58 +-------------- 4 files changed, 144 insertions(+), 126 deletions(-) create mode 100644 crates/lexarg-error/examples/hello-error.rs create mode 100644 crates/lexarg/examples/hello.rs diff --git a/crates/lexarg-error/examples/hello-error.rs b/crates/lexarg-error/examples/hello-error.rs new file mode 100644 index 0000000..c58c108 --- /dev/null +++ b/crates/lexarg-error/examples/hello-error.rs @@ -0,0 +1,80 @@ +use lexarg_error::ErrorContext; +use lexarg_error::Result; + +struct Args { + thing: String, + number: u32, + shout: bool, +} + +fn parse_args() -> Result { + #![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::>(); + 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))?, + ); + } + 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(()) +} diff --git a/crates/lexarg-error/src/lib.rs b/crates/lexarg-error/src/lib.rs index e881371..782f501 100644 --- a/crates/lexarg-error/src/lib.rs +++ b/crates/lexarg-error/src/lib.rs @@ -4,76 +4,9 @@ //! further so it can be used for CLI plugin systems. //! //! ## Example -//! ```no_run -//! use lexarg_error::Error; -//! use lexarg_error::ErrorContext; -//! use lexarg_error::Result; -//! -//! struct Args { -//! thing: String, -//! number: u32, -//! shout: bool, -//! } -//! -//! fn parse_args() -> Result { -//! use lexarg::Arg::*; //! -//! let mut thing = None; -//! let mut number = 1; -//! let mut shout = false; -//! let mut raw = std::env::args_os().collect::>(); -//! 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(lexarg::Arg::Value(value)).within(arg))? -//! .parse().map_err(|e| ErrorContext::msg(e).unexpected(lexarg::Arg::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))? -//! ); -//! } -//! Long("help") => { -//! println!("Usage: hello [-n|--number=NUM] [--shout] THING"); -//! std::process::exit(0); -//! } -//! _ => { -//! return Err(ErrorContext::msg("unexpected argument").unexpected(arg).within(lexarg::Arg::Value(bin_name)).into()); -//! } -//! } -//! } -//! -//! Ok(Args { -//! thing: thing.ok_or_else(|| ErrorContext::msg("missing argument THING").within(lexarg::Arg::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(()) -//! } +//! ```no_run +#![doc = include_str!("../examples/hello-error.rs")] //! ``` #![cfg_attr(docsrs, feature(doc_auto_cfg))] diff --git a/crates/lexarg/examples/hello.rs b/crates/lexarg/examples/hello.rs new file mode 100644 index 0000000..f47aed7 --- /dev/null +++ b/crates/lexarg/examples/hello.rs @@ -0,0 +1,61 @@ +struct Args { + thing: String, + number: u32, + shout: bool, +} + +fn parse_args() -> Result { + #![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::>(); + let mut parser = lexarg::Parser::new(&raw); + let _bin_name = parser.next_raw(); + while let Some(arg) = parser.next_arg() { + match arg { + Short("n") | Long("number") => { + number = parser + .next_flag_value() + .ok_or("`--number` requires a value")? + .to_str() + .ok_or("invalid number")? + .parse() + .map_err(|_e| "invalid number")?; + } + Long("shout") => { + shout = true; + } + Value(val) if thing.is_none() => { + thing = Some(val.to_str().ok_or("invalid string")?); + } + Long("help") => { + println!("Usage: hello [-n|--number=NUM] [--shout] THING"); + std::process::exit(0); + } + _ => { + return Err("unexpected argument"); + } + } + } + + Ok(Args { + thing: thing.ok_or("missing argument THING")?.to_owned(), + number, + shout, + }) +} + +fn main() -> Result<(), String> { + 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(()) +} diff --git a/crates/lexarg/src/lib.rs b/crates/lexarg/src/lib.rs index e369c33..91d68db 100644 --- a/crates/lexarg/src/lib.rs +++ b/crates/lexarg/src/lib.rs @@ -6,63 +6,7 @@ //! ## Example //! //! ```no_run -//! struct Args { -//! thing: String, -//! number: u32, -//! shout: bool, -//! } -//! -//! fn parse_args() -> Result { -//! use lexarg::Arg::*; -//! -//! let mut thing = None; -//! let mut number = 1; -//! let mut shout = false; -//! let mut raw = std::env::args_os().collect::>(); -//! let mut parser = lexarg::Parser::new(&raw); -//! let _bin_name = parser.next_raw(); -//! while let Some(arg) = parser.next_arg() { -//! match arg { -//! Short("n") | Long("number") => { -//! number = parser -//! .next_flag_value().ok_or("`--number` requires a value")? -//! .to_str().ok_or("invalid number")? -//! .parse().map_err(|_e| "invalid number")?; -//! } -//! Long("shout") => { -//! shout = true; -//! } -//! Value(val) if thing.is_none() => { -//! thing = Some(val.to_str().ok_or("invalid string")?); -//! } -//! Long("help") => { -//! println!("Usage: hello [-n|--number=NUM] [--shout] THING"); -//! std::process::exit(0); -//! } -//! _ => { -//! return Err("unexpected argument"); -//! } -//! } -//! } -//! -//! Ok(Args { -//! thing: thing.ok_or("missing argument THING")?.to_owned(), -//! number, -//! shout, -//! }) -//! } -//! -//! fn main() -> Result<(), String> { -//! 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(()) -//! } +#![doc = include_str!("../examples/hello.rs")] //! ``` #![cfg_attr(docsrs, feature(doc_auto_cfg))] From e40c0771ed1de2e85ae4c2acdcee6e09d94f6616 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 25 Mar 2025 16:02:20 -0500 Subject: [PATCH 33/35] feat(lexarg): Allow using Result in main --- crates/lexarg-error/src/lib.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/lexarg-error/src/lib.rs b/crates/lexarg-error/src/lib.rs index 782f501..facc35a 100644 --- a/crates/lexarg-error/src/lib.rs +++ b/crates/lexarg-error/src/lib.rs @@ -23,7 +23,6 @@ pub struct ReadmeDoctests; pub type Result = std::result::Result; /// Argument error type for use with lexarg -#[derive(Debug)] pub struct Error { msg: String, } @@ -48,6 +47,12 @@ impl From> for Error { } } +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) From b6b8b49552caa9eb6bb8b11da4baf0ecefc7c4a7 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 25 Mar 2025 16:08:09 -0500 Subject: [PATCH 34/35] docs(lexarg): Include short help --- crates/lexarg-error/examples/hello-error.rs | 2 +- crates/lexarg/examples/hello.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/lexarg-error/examples/hello-error.rs b/crates/lexarg-error/examples/hello-error.rs index c58c108..d75d4e7 100644 --- a/crates/lexarg-error/examples/hello-error.rs +++ b/crates/lexarg-error/examples/hello-error.rs @@ -45,7 +45,7 @@ fn parse_args() -> Result { .ok_or_else(|| ErrorContext::msg("invalid string").unexpected(arg))?, ); } - Long("help") => { + Short("h") | Long("help") => { println!("Usage: hello [-n|--number=NUM] [--shout] THING"); std::process::exit(0); } diff --git a/crates/lexarg/examples/hello.rs b/crates/lexarg/examples/hello.rs index f47aed7..25fda8e 100644 --- a/crates/lexarg/examples/hello.rs +++ b/crates/lexarg/examples/hello.rs @@ -31,7 +31,7 @@ fn parse_args() -> Result { Value(val) if thing.is_none() => { thing = Some(val.to_str().ok_or("invalid string")?); } - Long("help") => { + Short("h") | Long("help") => { println!("Usage: hello [-n|--number=NUM] [--shout] THING"); std::process::exit(0); } From 6eb05e8ea606ef752448fd4e0e0000f8181bfb7f Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 25 Mar 2025 16:47:40 -0500 Subject: [PATCH 35/35] docs(lexarg): Add more design justification --- DESIGN.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/DESIGN.md b/DESIGN.md index 102c58d..88a78f3 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -133,8 +133,10 @@ 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)`