diff --git a/DESIGN.md b/DESIGN.md index a251d8a..88a78f3 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -132,5 +132,20 @@ this makes delegating to plugins in a cooperative way more challenging. In reviewing lexopt's API: - Error handling is included in the API in a way that might make evolution difficult +- Escapes aren't explicitly communicated which makes communal parsing more difficult +- lexopt builds in specific option-value semantics -TODO: there were other points that felt off to me about lexopt's API wrt API stability but I do not recall what they are +And in general we will be putting the parser in the libtest-next's API and it will be a fundamental point of extension. +Having complete control helps ensure the full experience is cohesive. + +### Decision: `Short(&str)` + +`lexopt` and `clap` / `clap_lex` treat shorts as a `char` which gives a level of type safety to parsing. +However, with a minimal API, providing `&str` provides span information "for free". + +If someone were to make an API for pluggable lexers, +support for multi-character shorts is something people may want to opt-in to (it has been requested of clap). + +Performance isn't the top priority, so remoing `&str` -> `char` conversions isn't necessarily viewed as a benefit. +This also makes `match` need to work off of `&str` instead of `char`. +Unsure which of those would be slower and how the different characteristics match up. 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/examples/hello-error.rs b/crates/lexarg-error/examples/hello-error.rs new file mode 100644 index 0000000..d75d4e7 --- /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))?, + ); + } + Short("h") | Long("help") => { + println!("Usage: hello [-n|--number=NUM] [--shout] THING"); + std::process::exit(0); + } + _ => { + return Err(ErrorContext::msg("unexpected argument") + .unexpected(arg) + .within(Value(bin_name)) + .into()); + } + } + } + + Ok(Args { + thing: thing + .ok_or_else(|| ErrorContext::msg("missing argument THING").within(Value(bin_name)))? + .to_owned(), + number, + shout, + }) +} + +fn main() -> Result<()> { + let args = parse_args()?; + let mut message = format!("Hello {}", args.thing); + if args.shout { + message = message.to_uppercase(); + } + for _ in 0..args.number { + println!("{message}"); + } + Ok(()) +} diff --git a/crates/lexarg-error/src/lib.rs b/crates/lexarg-error/src/lib.rs index 4ce37f8..facc35a 100644 --- a/crates/lexarg-error/src/lib.rs +++ b/crates/lexarg-error/src/lib.rs @@ -1,70 +1,12 @@ -//! Argument error type for use with lexarg +//! Error type for use with lexarg //! //! Inspired by [lexopt](https://crates.io/crates/lexopt), `lexarg` simplifies the formula down //! further so it can be used for CLI plugin systems. //! //! ## Example -//! ```no_run -//! use lexarg_error::Error; -//! use lexarg_error::Result; -//! -//! struct Args { -//! thing: String, -//! number: u32, -//! shout: bool, -//! } -//! -//! fn parse_args() -> Result { -//! 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); -//! parser.bin(); -//! while let Some(arg) = parser.next() { -//! match arg { -//! Short('n') | Long("number") => { -//! number = parser -//! .flag_value().ok_or_else(|| Error::msg("`--number` requires a value"))? -//! .to_str().ok_or_else(|| Error::msg("invalid number"))? -//! .parse().map_err(|e| Error::msg(e))?; -//! } -//! Long("shout") => { -//! shout = true; -//! } -//! Value(val) if thing.is_none() => { -//! thing = Some(val.to_str().ok_or_else(|| Error::msg("invalid number"))?); -//! } -//! Long("help") => { -//! println!("Usage: hello [-n|--number=NUM] [--shout] THING"); -//! std::process::exit(0); -//! } -//! _ => { -//! return Err(Error::msg("unexpected argument")); -//! } -//! } -//! } -//! -//! Ok(Args { -//! thing: thing.ok_or_else(|| Error::msg("missing argument THING"))?.to_owned(), -//! number, -//! shout, -//! }) -//! } -//! -//! fn main() -> Result<()> { -//! let args = parse_args()?; -//! let mut message = format!("Hello {}", args.thing); -//! if args.shout { -//! message = message.to_uppercase(); -//! } -//! for _ in 0..args.number { -//! println!("{}", message); -//! } -//! Ok(()) -//! } +//! ```no_run +#![doc = include_str!("../examples/hello-error.rs")] //! ``` #![cfg_attr(docsrs, feature(doc_auto_cfg))] @@ -77,55 +19,10 @@ #[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 -#[derive(Debug)] pub struct Error { msg: String, } @@ -137,24 +34,105 @@ impl Error { where M: std::fmt::Display, { - Error { + Self { msg: message.to_string(), } } } -impl From for Error +impl From> for Error { + #[cold] + fn from(error: ErrorContext<'_>) -> Self { + Self::msg(error.to_string()) + } +} + +impl std::fmt::Debug for Error { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.msg.fmt(formatter) + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.msg.fmt(formatter) + } +} + +/// Collect context for creating an [`Error`] +#[derive(Debug)] +pub struct ErrorContext<'a> { + msg: String, + within: Option>, + unexpected: Option>, +} + +impl<'a> ErrorContext<'a> { + /// 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(), + within: None, + unexpected: None, + } + } + + /// [`Arg`][lexarg::Arg] the error occurred within + #[cold] + pub fn within(mut self, within: lexarg::Arg<'a>) -> Self { + self.within = Some(within); + self + } + + /// The failing [`Arg`][lexarg::Arg] + #[cold] + pub fn unexpected(mut self, unexpected: lexarg::Arg<'a>) -> Self { + self.unexpected = Some(unexpected); + self + } +} + +impl From for ErrorContext<'_> where E: std::error::Error + Send + Sync + 'static, { #[cold] fn from(error: E) -> Self { - Error::msg(error) + Self::msg(error) } } -impl std::fmt::Display for Error { +impl std::fmt::Display for ErrorContext<'_> { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.msg.fmt(formatter) + self.msg.fmt(formatter)?; + if let Some(unexpected) = &self.unexpected { + write!(formatter, ", found `")?; + match unexpected { + lexarg::Arg::Short(short) => write!(formatter, "-{short}")?, + lexarg::Arg::Long(long) => write!(formatter, "--{long}")?, + lexarg::Arg::Escape(value) => write!(formatter, "{value}")?, + lexarg::Arg::Value(value) | lexarg::Arg::Unexpected(value) => { + write!(formatter, "{}", value.to_string_lossy())?; + } + } + write!(formatter, "`")?; + } + if let Some(within) = &self.within { + write!(formatter, " when parsing `")?; + match within { + lexarg::Arg::Short(short) => write!(formatter, "-{short}")?, + lexarg::Arg::Long(long) => write!(formatter, "--{long}")?, + lexarg::Arg::Escape(value) => write!(formatter, "{value}")?, + lexarg::Arg::Value(value) | lexarg::Arg::Unexpected(value) => { + write!(formatter, "{}", value.to_string_lossy())?; + } + } + write!(formatter, "`")?; + } + Ok(()) } } diff --git a/crates/lexarg/examples/hello.rs b/crates/lexarg/examples/hello.rs new file mode 100644 index 0000000..25fda8e --- /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")?); + } + Short("h") | 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 6dc1052..91d68db 100644 --- a/crates/lexarg/src/lib.rs +++ b/crates/lexarg/src/lib.rs @@ -4,67 +4,13 @@ //! further so it can be used for CLI plugin systems. //! //! ## 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); -//! parser.bin(); -//! while let Some(arg) = parser.next() { -//! match arg { -//! Short('n') | Long("number") => { -//! number = parser -//! .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 number")?); -//! } -//! 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(()) -//! } +//! ```no_run +#![doc = include_str!("../examples/hello.rs")] //! ``` #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![allow(clippy::result_unit_err)] #![warn(missing_debug_implementations)] #![warn(missing_docs)] #![warn(clippy::print_stderr)] @@ -110,24 +56,16 @@ impl<'a> Parser<'a> { } } - /// Extract the binary name before parsing [`Arg`]s + /// Get the next option or positional [`Arg`]. /// - /// # Panic + /// Returns `None` if the command line has been exhausted. /// - /// 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 argument. + /// Returns [`Arg::Unexpected`] on failure /// - /// 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`]. - #[allow(clippy::should_implement_trait)] - pub fn next(&mut self) -> Option> { + /// 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; @@ -139,48 +77,26 @@ 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 `.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 matches!(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; - + 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 self.state = None; self.current += 1; - Some(Arg::Unexpected(unexpected)) - } 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)); - } else { - // No more flags - if invalid.is_empty() { - self.state = None; - self.current += 1; - } else { - self.state = Some(State::PendingValue(invalid)); - } - } - Some(Arg::Short(short)) } + let flag = &valid[index..next_index]; + Some(Arg::Short(flag)) } 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) @@ -197,14 +113,14 @@ 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)?; 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; @@ -235,7 +151,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; @@ -248,32 +164,35 @@ 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 `-`). /// /// `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`) - pub fn flag_value(&mut self) -> Option<&'a OsStr> { - if let Some(value) = self.next_attached_value() { - self.was_attached = true; - return Some(value); - } - - if !self.was_attached { - return self.next_value(); + /// - 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()); + None + } else if let Some(value) = self.next_attached_value() { + Some(value) + } else { + self.next_detached_value() } - - None } - 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; self.current += 1; + self.was_attached = true; Some(attached) } State::PendingShorts(_, _, index) => { @@ -288,40 +207,76 @@ 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); + self.was_attached = true; Some(remainder) } } - State::Escaped => { - self.state = Some(State::Escaped); - None - } + State::Escaped => None, } } - 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; } - let next = self.next_raw()?; - - if next == "--" { - self.state = Some(State::Escaped); + if self.peek_raw_()? == "--" { None } else { - Some(next) + self.next_raw_() + } + } + + /// Get the next argument, independent of what it looks like + /// + /// Returns `Err(())` if an [attached value][Parser::next_attached_value] is present + pub fn next_raw(&mut self) -> Result, ()> { + if self.has_pending() { + Err(()) + } else { + self.was_attached = false; + Ok(self.next_raw_()) + } + } + + /// 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_())) + } + } + + /// 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 next_raw(&mut self) -> Option<&'a OsStr> { + 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); + 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) } @@ -406,19 +361,18 @@ 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, } impl State<'_> { - #[cfg(test)] fn has_pending(&self) -> bool { match self { Self::PendingValue(_) | Self::PendingShorts(_, _, _) => true, @@ -427,17 +381,19 @@ impl State<'_> { } } -/// A command line argument found by [`Parser`], either an option or a positional argument. -#[derive(Debug, Clone, PartialEq, Eq)] +/// A command line argument found by [`Parser`], either an option or a positional argument +#[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum Arg<'a> { - /// 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 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 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, + Escape(&'a str), /// User passed something in that doesn't work Unexpected(&'a OsStr), } @@ -454,6 +410,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; @@ -471,88 +431,90 @@ 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.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(), 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("-"))); + 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.flag_value().unwrap(), "value"); - assert_eq!(p.next().unwrap(), Short('x')); - assert_eq!(p.next().unwrap(), Short('f')); - assert_eq!(p.flag_value().unwrap(), "value"); - assert_eq!(p.next(), None); + 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_flag_value().unwrap(), "value"); + 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.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("foo")); + assert_eq!(p.next_arg().unwrap(), Long("bar")); + 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); } #[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.flag_value(), None); - 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_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); // "-" 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(), 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().unwrap(), Short('o')); - assert_eq!(p.flag_value(), None); + assert_eq!(p.next_arg().unwrap(), Short("o")); + assert_eq!(p.next_flag_value(), None); let mut q = Parser::new(&["--out"]); - assert_eq!(q.next().unwrap(), Long("out")); - assert_eq!(q.flag_value(), None); + assert_eq!(q.next_arg().unwrap(), Long("out")); + 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] @@ -560,38 +522,38 @@ 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.flag_value().unwrap(), "10"); - assert_eq!(p.next().unwrap(), Value(OsStr::new("µ"))); - assert_eq!(p.next().unwrap(), Long("foo")); - assert_eq!(p.flag_value().unwrap(), "µ"); + 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("µ"))); + assert_eq!(p.next_arg().unwrap(), Long("foo")); + assert_eq!(p.next_flag_value().unwrap(), "µ"); } #[cfg(any(unix, target_os = "wasi", windows))] @@ -599,24 +561,24 @@ 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.flag_value().unwrap(), bad_string("@@@")); + assert_eq!(p.next_arg().unwrap(), Long("foo")); + assert_eq!(p.next_flag_value().unwrap(), bad_string("@@@")); let args = [bad_string("-💣@@@")]; let mut q = Parser::new(&args); - assert_eq!(q.next().unwrap(), Short('💣')); - assert_eq!(q.flag_value().unwrap(), bad_string("@@@")); + 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().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.flag_value().unwrap(), bad_string("bar=@@@")); + assert_eq!(s.next_arg().unwrap(), Long("foo")); + assert_eq!(s.next_flag_value().unwrap(), bad_string("bar=@@@")); } #[cfg(any(unix, target_os = "wasi", windows))] @@ -624,8 +586,8 @@ 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.flag_value().unwrap(), bad_string("@@@")); + assert_eq!(p.next_arg().unwrap(), Long("foo")); + assert_eq!(p.next_flag_value().unwrap(), bad_string("@@@")); } #[cfg(any(unix, target_os = "wasi", windows))] @@ -633,13 +595,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))] @@ -647,46 +609,65 @@ 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.flag_value().unwrap(), OsStr::new("b")); - assert_eq!(p.next(), None); + 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().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(), 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().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(), 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().unwrap(), Short('a')); - assert_eq!(p.flag_value().unwrap(), OsStr::new("")); - assert_eq!(p.next(), None); + 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_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("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_flag_value().unwrap(), OsStr::new("=de")); + 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(), Short("=")); + 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(), Short("=")); + 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(), Short("=")); + assert_eq!(p.next_arg().unwrap(), Short("a")); + assert_eq!(p.next_arg(), None); } #[cfg(any(unix, target_os = "wasi", windows))] @@ -695,14 +676,38 @@ 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.flag_value().unwrap(), bad_string("@")); - assert_eq!(p.next(), None); + 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().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(), Short("=")); + assert_eq!(p.next_arg().unwrap(), Unexpected(&bad)); + 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. @@ -800,7 +805,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:?}", @@ -812,7 +817,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:?}")); @@ -821,7 +826,7 @@ mod tests { } else { { let mut parser = parser.clone(); - let next = parser.next(); + let next = parser.next_arg(); match &next { None => { assert!( @@ -840,14 +845,17 @@ mod tests { { let mut parser = parser.clone(); - let next = parser.flag_value(); + let next = parser.next_flag_value(); match &next { None => { assert!( 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:?}",); } } diff --git a/crates/libtest-lexarg/src/lib.rs b/crates/libtest-lexarg/src/lib.rs index d5f8e4b..5591f82 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`") })? @@ -376,14 +376,14 @@ impl TestOptsParseState { } }; } - Arg::Short('q') | Arg::Long("quiet") => { + Arg::Short("q") | Arg::Long("quiet") => { self.format = None; self.quiet = true; } 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`", @@ -406,9 +406,9 @@ impl TestOptsParseState { Arg::Long("show-output") => { self.opts.options.display_output = true; } - Arg::Short('Z') => { + 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"))? @@ -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 7840ef4..d84527d 100644 --- a/crates/libtest2-harness/src/harness.rs +++ b/crates/libtest2-harness/src/harness.rs @@ -73,10 +73,10 @@ 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(); - while let Some(arg) = parser.next() { + 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(); @@ -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()) }