From 34d8b2ca7ab2894ba3e3981b34b339e391e94b5b Mon Sep 17 00:00:00 2001 From: esther-ff Date: Fri, 27 Jun 2025 19:12:52 +0200 Subject: [PATCH 1/2] switched `String` in `ParsingError` to `Cow` --- src/lib.rs | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 7df0871..c737358 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,7 +62,7 @@ use std::{ /// Ok("config".to_string()) /// } /// ``` -pub type Result = core::result::Result; +pub type Result> = core::result::Result; /// Represents a parsed command-line argument. /// @@ -138,7 +138,7 @@ impl<'a> Argument<'a> { /// let error = arg.into_error(None::<&str>); /// assert_eq!(error.to_string(), "unexpected argument: -"); /// ``` - pub fn into_error(self, value: A) -> ParsingError + pub fn into_error(self, value: A) -> ParsingError<'static> where A: Into>, { @@ -146,25 +146,25 @@ impl<'a> Argument<'a> { match self { Long(arg) => ParsingError::UnexpectedArg { - offender: arg.to_string(), - value: value.into().map(String::from), + offender: Cow::Owned(arg.to_string()), + value: value.into().map(String::from).map(Cow::Owned), format: "=", prefix: "--", }, Short(arg) => ParsingError::UnexpectedArg { - offender: arg.to_string(), - value: value.into().map(String::from), + offender: Cow::Owned(arg.to_string()), + value: value.into().map(String::from).map(Cow::Owned), format: " ", prefix: "-", }, Value(arg) => ParsingError::UnexpectedArg { - offender: arg.to_string(), + offender: Cow::Owned(arg.to_string()), value: None, format: "", prefix: "", }, Stdio => ParsingError::UnexpectedArg { - offender: "-".to_string(), + offender: Cow::Borrowed("-"), value: None, format: "", prefix: "", @@ -393,7 +393,7 @@ where if let State::LeftoverValue(ref mut value) = self.state { return Err(ParsingError::UnconsumedValue { - value: mem::take(value), + value: Cow::Owned(mem::take(value)), }); } @@ -518,7 +518,7 @@ where /// All parsing operations return a `Result` with this error type. Each variant /// provides specific context about what went wrong during parsing. #[derive(Debug)] -pub enum ParsingError { +pub enum ParsingError<'a> { /// Invalid option syntax or format was encountered. /// /// This typically occurs when: @@ -531,7 +531,7 @@ pub enum ParsingError { /// * `offender` - The specific argument that caused the error (if available) InvalidOption { reason: &'static str, - offender: Option, + offender: Option>, }, /// An option value was not consumed after being parsed. @@ -543,7 +543,7 @@ pub enum ParsingError { /// # Fields /// /// * `value` - The unconsumed value that was attached to the option - UnconsumedValue { value: String }, + UnconsumedValue { value: Cow<'a, str> }, /// The argument iterator was empty (contained no program name). /// @@ -563,14 +563,14 @@ pub enum ParsingError { /// * `format` - How the value is formatted in error messages (e.g., "=" or " ") /// * `prefix` - The argument prefix (e.g., "--" for long options, "-" for short) UnexpectedArg { - offender: String, - value: Option, + offender: Cow<'a, str>, + value: Option>, format: &'static str, prefix: &'static str, }, } -impl Display for ParsingError { +impl Display for ParsingError<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::InvalidOption { reason, offender } => { @@ -606,7 +606,7 @@ impl Display for ParsingError { } } -impl Error for ParsingError {} +impl Error for ParsingError<'_> {} #[cfg(test)] mod tests { From d8b26a6747f6a9d6fe60d2bab7374f1439d52658 Mon Sep 17 00:00:00 2001 From: esther-ff Date: Fri, 27 Jun 2025 20:15:40 +0200 Subject: [PATCH 2/2] GNU error message generation --- src/lib.rs | 147 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 137 insertions(+), 10 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index c737358..ec34742 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,7 +44,7 @@ use std::{ error::Error, fmt::{Debug, Display}, hint::unreachable_unchecked, - mem, + io, mem, }; /// A [`Result`] type alias using [`ParsingError`] as the default error type. @@ -352,7 +352,7 @@ where if char == '=' { return Err(ParsingError::InvalidOption { reason: "Short options do not support values", - offender: None, + offender: Cow::Borrowed("="), }); } @@ -393,6 +393,7 @@ where if let State::LeftoverValue(ref mut value) = self.state { return Err(ParsingError::UnconsumedValue { + option_name: Cow::Owned(rest.to_owned()), value: Cow::Owned(mem::take(value)), }); } @@ -531,7 +532,7 @@ pub enum ParsingError<'a> { /// * `offender` - The specific argument that caused the error (if available) InvalidOption { reason: &'static str, - offender: Option>, + offender: Cow<'a, str>, }, /// An option value was not consumed after being parsed. @@ -543,7 +544,10 @@ pub enum ParsingError<'a> { /// # Fields /// /// * `value` - The unconsumed value that was attached to the option - UnconsumedValue { value: Cow<'a, str> }, + UnconsumedValue { + option_name: Cow<'a, str>, + value: Cow<'a, str>, + }, /// The argument iterator was empty (contained no program name). /// @@ -575,15 +579,13 @@ impl Display for ParsingError<'_> { match self { Self::InvalidOption { reason, offender } => { write!(f, "reason: {reason}")?; - if let Some(sentence) = offender { - write!(f, " at: {sentence}")?; - } + write!(f, " at: {offender}")?; Ok(()) } - Self::UnconsumedValue { value } => { - write!(f, "leftover value: {value}",) + Self::UnconsumedValue { option_name, value } => { + write!(f, "leftover value: {value} for option {option_name}",) } Self::UnexpectedArg { @@ -608,9 +610,76 @@ impl Display for ParsingError<'_> { impl Error for ParsingError<'_> {} +impl ParsingError<'_> { + /// Writes a GNU-like error message into `w` + /// + /// # Errors + /// + /// To simplify the return type, this function returns `io::ErrorKind::NotFound` when it cannot + /// write an error to `w` because of the lack of a proper GNU-style representation for it. + /// Currently this only happens to the `ParsingError::Empty` variant. + /// + /// Any other error from `io::ErrorKind` signifies a failure coming from the underlying writer provided + /// via the `w` argument. + /// + pub fn gnu_error(&self, w: &mut W, name: &str) -> std::io::Result<()> + where + W: io::Write, + { + match self { + // no GNU representation for this one + ParsingError::Empty => return Err(io::ErrorKind::NotFound.into()), + + ParsingError::UnconsumedValue { option_name, .. } => { + writeln!( + w, + "{name}: option '--{option_name}' doesn't allow an argument" + ) + } + + ParsingError::InvalidOption { offender, .. } => { + writeln!(w, "{name}: invalid option -- '{offender}'") + } + + ParsingError::UnexpectedArg { offender, .. } => { + writeln!(w, "{name}: unrecognized option '{offender}'") + } + }?; + + writeln!(w, "Try '{name} --help' for more information.").map_err(Into::into) + } + + /// Produces a `Vec` containing a GNU-style error message. + /// + /// This function returns a `Vec` instead of a `String` due + /// to the lack of guarantee that the option name will be proper UTF-8. + /// + /// # Errors + /// + /// To simplify the return type, this function returns `io::ErrorKind::NotFound` when it cannot + /// write an error to `w` because of the lack of a proper GNU-style representation for it. + /// Currently this only happens to the `ParsingError::Empty` variant. + #[must_use] + pub fn gnu_error_as_vec(&self, name: &str) -> Option> { + let mut buf = Vec::new(); + + match self.gnu_error(&mut buf, name) { + Ok(()) => Some(buf), + + Err(err) => match err.kind() { + io::ErrorKind::NotFound => None, + + _ => unreachable!( + "any other errors here are impossible, due to `io::Write::write` being infallible as far as `Result` is concerned" + ), + }, + } + } +} + #[cfg(test)] mod tests { - use crate::{Argument::*, Parser, Result}; + use crate::{Argument::*, Parser, ParsingError, Result}; #[test] fn parser_creation() -> Result<()> { @@ -893,4 +962,62 @@ mod tests { } Ok(()) } + + #[test] + fn gnu_error_unrecognized_option() -> Result<()> { + const CMP: &str = "error_program: unrecognized option 'error'\n\ + Try 'error_program --help' for more information.\n"; + + let bytes = Long("error") + .into_error("not_important_for_this_test") + .gnu_error_as_vec("error_program") + .expect("couldn't process the error into a GNU error message"); + + let error_string = core::str::from_utf8(&bytes) + .expect("the produced `Vec` from the error, contained invalid UTF-8"); + + assert_eq!(error_string, CMP, "the produced error was invalid"); + + Ok(()) + } + + #[test] + fn gnu_error_invalid_option() -> Result<()> { + const CMP: &str = "error_program: invalid option -- '='\n\ + Try 'error_program --help' for more information.\n"; + + let bytes = ParsingError::InvalidOption { + reason: "not_important_for_this_test", + offender: "=".into(), + } + .gnu_error_as_vec("error_program") + .expect("couldn't process the error into a GNU error message"); + + let error_string = core::str::from_utf8(&bytes) + .expect("the produced `Vec` from the error, contained invalid UTF-8"); + + assert_eq!(error_string, CMP, "the produced error was invalid"); + + Ok(()) + } + + #[test] + fn gnu_error_leftover_value() -> Result<()> { + const CMP: &str = "error_program: option '--fallible' doesn't allow an argument\n\ + Try 'error_program --help' for more information.\n"; + + let bytes = ParsingError::UnconsumedValue { + option_name: "fallible".into(), + value: "not_important_for_this_test".into(), + } + .gnu_error_as_vec("error_program") + .expect("couldn't process the error into a GNU error message"); + + let error_string = core::str::from_utf8(&bytes) + .expect("the produced `Vec` from the error, contained invalid UTF-8"); + + assert_eq!(error_string, CMP, "the produced error was invalid"); + + Ok(()) + } }