Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 151 additions & 24 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -62,7 +62,7 @@ use std::{
/// Ok("config".to_string())
/// }
/// ```
pub type Result<T, E = ParsingError> = core::result::Result<T, E>;
pub type Result<T, E = ParsingError<'static>> = core::result::Result<T, E>;

/// Represents a parsed command-line argument.
///
Expand Down Expand Up @@ -138,33 +138,33 @@ impl<'a> Argument<'a> {
/// let error = arg.into_error(None::<&str>);
/// assert_eq!(error.to_string(), "unexpected argument: -");
/// ```
pub fn into_error<A>(self, value: A) -> ParsingError
pub fn into_error<A>(self, value: A) -> ParsingError<'static>
where
A: Into<Option<&'a str>>,
{
use Argument::{Long, Short, Stdio, Value};

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: "",
Expand Down Expand Up @@ -352,7 +352,7 @@ where
if char == '=' {
return Err(ParsingError::InvalidOption {
reason: "Short options do not support values",
offender: None,
offender: Cow::Borrowed("="),
});
}

Expand Down Expand Up @@ -393,7 +393,8 @@ where

if let State::LeftoverValue(ref mut value) = self.state {
return Err(ParsingError::UnconsumedValue {
value: mem::take(value),
option_name: Cow::Owned(rest.to_owned()),
value: Cow::Owned(mem::take(value)),
});
}

Expand Down Expand Up @@ -518,7 +519,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:
Expand All @@ -531,7 +532,7 @@ pub enum ParsingError {
/// * `offender` - The specific argument that caused the error (if available)
InvalidOption {
reason: &'static str,
offender: Option<String>,
offender: Cow<'a, str>,
},

/// An option value was not consumed after being parsed.
Expand All @@ -543,7 +544,10 @@ pub enum ParsingError {
/// # Fields
///
/// * `value` - The unconsumed value that was attached to the option
UnconsumedValue { value: String },
UnconsumedValue {
option_name: Cow<'a, str>,
value: Cow<'a, str>,
},

/// The argument iterator was empty (contained no program name).
///
Expand All @@ -563,27 +567,25 @@ 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<String>,
offender: Cow<'a, str>,
value: Option<Cow<'a, str>>,
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 } => {
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 {
Expand All @@ -606,11 +608,78 @@ impl Display for ParsingError {
}
}

impl Error 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<W>(&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<u8>` containing a GNU-style error message.
///
/// This function returns a `Vec<u8>` 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<Vec<u8>> {
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<T, E>` is concerned"
),
},
}
}
}

#[cfg(test)]
mod tests {
use crate::{Argument::*, Parser, Result};
use crate::{Argument::*, Parser, ParsingError, Result};

#[test]
fn parser_creation() -> Result<()> {
Expand Down Expand Up @@ -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(())
}
}