use std::fmt;
use winnow::error::{FromExternalError, ModalError, ParserError};
use crate::help::Style;
use crate::stream::Argv;
mod suggest;
pub type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ErrorKind {
Backtrack,
UnknownFlag,
UnexpectedArg,
MissingValue,
UnexpectedValue,
MissingRequired,
MissingArgument,
MissingSubcommand,
Conflict,
MissingOneOf,
RequiresDoubleDash,
HelpRequested,
VersionRequested,
InvalidValue,
InvalidChoice,
}
#[derive(Debug)]
struct Request {
command: &'static crate::help::Command,
long: bool,
bare: bool,
path: Vec<&'static str>,
}
#[derive(Debug, Default)]
struct Detail {
request: Option<Request>,
token: String,
value: Option<String>,
cause: Option<BoxError>,
path: Vec<&'static str>,
similar: Option<(&'static str, String)>,
}
#[derive(Debug)]
pub struct Error {
kind: ErrorKind,
cut: bool,
offset: usize,
detail: Option<Box<Detail>>,
}
impl Error {
fn with_token(kind: ErrorKind, offset: usize, token: String) -> Self {
Self {
kind,
cut: true,
offset,
detail: Some(Box::new(Detail {
token,
..Detail::default()
})),
}
}
pub fn unknown_flag(offset: usize, token: impl Into<String>) -> Self {
Self::with_token(ErrorKind::UnknownFlag, offset, token.into())
}
pub fn unexpected_arg(offset: usize, token: impl Into<String>) -> Self {
Self::with_token(ErrorKind::UnexpectedArg, offset, token.into())
}
pub fn missing_value(offset: usize, flag: impl Into<String>) -> Self {
Self::with_token(ErrorKind::MissingValue, offset, flag.into())
}
pub fn unexpected_value(offset: usize, flag: impl Into<String>, value: &[u8]) -> Self {
let mut error = Self::with_token(ErrorKind::UnexpectedValue, offset, flag.into());
error.detail_mut().value = Some(String::from_utf8_lossy(value).into_owned());
error
}
pub fn missing_required(offset: usize, flag: impl Into<String>) -> Self {
Self::with_token(ErrorKind::MissingRequired, offset, flag.into())
}
pub fn missing_argument(offset: usize, name: impl Into<String>) -> Self {
Self::with_token(ErrorKind::MissingArgument, offset, name.into())
}
pub fn required_by(offset: usize, target: impl Into<String>, name: &str) -> Self {
let mut error = Self::with_token(ErrorKind::MissingRequired, offset, target.into());
error.detail_mut().value = Some(name.to_owned());
error
}
pub fn conflict(offset: usize, name: impl Into<String>, other: &str) -> Self {
let mut error = Self::with_token(ErrorKind::Conflict, offset, name.into());
error.detail_mut().value = Some(other.to_owned());
error
}
pub fn missing_one_of(offset: usize, group: impl Into<String>, members: &[&str]) -> Self {
let mut error = Self::with_token(ErrorKind::MissingOneOf, offset, group.into());
error.detail_mut().value = Some(members.join(", "));
error
}
pub fn requires_double_dash(offset: usize, name: impl Into<String>) -> Self {
Self::with_token(ErrorKind::RequiresDoubleDash, offset, name.into())
}
fn request(
kind: ErrorKind,
command: &'static crate::help::Command,
long: bool,
bare: bool,
) -> Self {
Self {
kind,
cut: true,
offset: 0,
detail: Some(Box::new(Detail {
request: Some(Request {
command,
long,
bare,
path: Vec::new(),
}),
..Detail::default()
})),
}
}
pub fn help(command: &'static crate::help::Command, long: bool) -> Self {
Self::request(ErrorKind::HelpRequested, command, long, false)
}
pub fn bare_help(command: &'static crate::help::Command) -> Self {
Self::request(ErrorKind::HelpRequested, command, false, true)
}
pub fn version(command: &'static crate::help::Command) -> Self {
Self::request(ErrorKind::VersionRequested, command, false, false)
}
#[cold]
#[inline(never)]
pub fn within(mut self, name: &'static str) -> Self {
if let Some(detail) = self.detail.as_deref_mut() {
match detail.request.as_mut() {
Some(request) => request.path.insert(0, name),
None => detail.path.insert(0, name),
}
}
self
}
#[cold]
#[inline(never)]
pub fn suggested(mut self, root: &'static crate::help::Command) -> Self {
let Some(detail) = self.detail.as_deref_mut() else {
return self;
};
let found = match self.kind {
ErrorKind::UnknownFlag => {
suggest::flag(root, &detail.path, &detail.token).map(|flag| ("flag", flag))
}
ErrorKind::UnexpectedArg => suggest::subcommand(root, &detail.path, &detail.token)
.map(|name| ("subcommand", name.to_owned())),
_ => None,
};
if let Some(found) = found {
detail.similar = Some(found);
}
self
}
pub fn similar(&self) -> Option<&str> {
let (_, similar) = self.detail.as_deref()?.similar.as_ref()?;
Some(similar)
}
pub fn render_help(&self, program: &str) -> Option<String> {
self.render_help_styled(program, Style::PLAIN)
}
pub fn version_text(&self, program: &str) -> Option<String> {
let request = self.detail.as_deref()?.request.as_ref()?;
let version = request.command.version?;
(self.kind == ErrorKind::VersionRequested).then(|| format!("{program} {version}"))
}
pub fn is_bare_help(&self) -> bool {
self.detail
.as_deref()
.and_then(|d| d.request.as_ref())
.is_some_and(|r| r.bare)
}
pub fn missing_subcommand(offset: usize) -> Self {
Self {
kind: ErrorKind::MissingSubcommand,
cut: true,
offset,
detail: None,
}
}
pub fn invalid_value(
offset: usize,
flag: impl Into<String>,
value: &[u8],
cause: impl Into<BoxError>,
) -> Self {
let cause = cause.into();
let choices = cause.downcast_ref::<crate::value::ChoiceError>();
let kind = match choices {
Some(_) => ErrorKind::InvalidChoice,
None => ErrorKind::InvalidValue,
};
let value = String::from_utf8_lossy(value).into_owned();
let similar = choices
.and_then(|error| suggest::nearest(&value, error.choices.iter().copied()))
.map(|choice| ("value", choice.to_owned()));
let mut error = Self::with_token(kind, offset, flag.into());
let detail = error.detail_mut();
detail.value = Some(value);
detail.cause = Some(cause);
detail.similar = similar;
error
}
fn detail_mut(&mut self) -> &mut Detail {
self.detail.get_or_insert_with(Default::default)
}
pub fn kind(&self) -> ErrorKind {
self.kind
}
pub fn offset(&self) -> usize {
self.offset
}
pub fn token(&self) -> Option<&str> {
self.detail.as_deref().map(|d| d.token.as_str())
}
pub fn value(&self) -> Option<&str> {
self.detail.as_deref()?.value.as_deref()
}
pub fn is_cut(&self) -> bool {
self.cut
}
}
impl PartialEq for Error {
fn eq(&self, other: &Self) -> bool {
self.kind == other.kind
&& self.offset == other.offset
&& self.token() == other.token()
&& self.value() == other.value()
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message(Style::PLAIN))
}
}
impl Error {
pub fn message(&self, style: Style) -> String {
let bad = |text: &str| Style::paint(style.invalid(), text);
let good = |text: &str| Style::paint(style.valid(), text);
let token = self.token().unwrap_or_default();
let value = self.value().unwrap_or_default();
match self.kind {
ErrorKind::Backtrack => "invalid command line".to_owned(),
ErrorKind::UnknownFlag => format!("unknown flag `{}`", bad(token)),
ErrorKind::UnexpectedArg => format!("unexpected argument `{}`", bad(token)),
ErrorKind::MissingValue => format!("`{}` needs a value", good(token)),
ErrorKind::UnexpectedValue => format!(
"`{}` does not take a value, got `{}`",
bad(token),
bad(value)
),
ErrorKind::MissingRequired => match self.value() {
Some(by) => format!("`{}` is required by `{}`", good(token), bad(by)),
None => format!("`{}` is required", good(token)),
},
ErrorKind::Conflict => format!("`{}` cannot be used with `{}`", bad(token), bad(value)),
ErrorKind::HelpRequested => "help requested".to_owned(),
ErrorKind::VersionRequested => "version requested".to_owned(),
ErrorKind::RequiresDoubleDash => {
format!("`{}` can only be set after a `--` separator", bad(token))
}
ErrorKind::MissingOneOf => format!("one of {} is required ({token})", good(value)),
ErrorKind::MissingArgument => format!("missing argument `{}`", good(token)),
ErrorKind::MissingSubcommand => "a subcommand is required".to_owned(),
ErrorKind::InvalidValue | ErrorKind::InvalidChoice => {
let mut text = format!("invalid value `{}` for `{token}`", bad(value));
if let Some(cause) = self.detail.as_deref().and_then(|d| d.cause.as_ref()) {
text.push_str(": ");
text.push_str(&cause.to_string());
}
text
}
}
}
pub fn tip(&self, style: Style) -> Option<String> {
let (what, similar) = self.detail.as_deref()?.similar.as_ref()?;
Some(format!(
"{} a similar {what} exists: `{}`",
Style::paint(style.valid(), "tip:"),
Style::paint(style.valid(), similar),
))
}
pub fn render(&self, style: Style) -> String {
let tip = self.tip(style).map(|tip| format!("\n\n {tip}"));
format!(
"{} {}{}\n\nFor more information, try '{}'.",
Style::paint(style.error(), "error:"),
self.message(style),
tip.unwrap_or_default(),
Style::paint(style.flag(), "--help"),
)
}
pub fn render_help_styled(&self, program: &str, style: Style) -> Option<String> {
let request = self.detail.as_deref()?.request.as_ref()?;
if self.kind != ErrorKind::HelpRequested {
return None;
}
let mut path = vec![program];
path.extend(&request.path);
let width = crate::help::width();
Some(crate::help::render_styled(
request.command,
&path,
request.long,
width,
style,
))
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
let cause = self.detail.as_deref()?.cause.as_ref()?;
Some(&**cause as &(dyn std::error::Error + 'static))
}
}
impl<'i> ParserError<Argv<'i>> for Error {
type Inner = Self;
#[inline]
fn from_input(input: &Argv<'i>) -> Self {
Self {
kind: ErrorKind::Backtrack,
cut: false,
offset: input.offset(),
detail: None,
}
}
#[inline]
fn or(self, other: Self) -> Self {
if other.kind == ErrorKind::Backtrack && self.kind != ErrorKind::Backtrack {
self
} else {
other
}
}
#[inline(always)]
fn is_backtrack(&self) -> bool {
!self.cut
}
#[inline(always)]
fn into_inner(self) -> Result<Self::Inner, Self> {
Ok(self)
}
}
impl ModalError for Error {
#[inline(always)]
fn cut(mut self) -> Self {
self.cut = true;
self
}
#[inline(always)]
fn backtrack(mut self) -> Self {
self.cut = false;
self
}
}
impl<'i, E> FromExternalError<Argv<'i>, E> for Error
where
E: std::error::Error + Send + Sync + 'static,
{
fn from_external_error(input: &Argv<'i>, e: E) -> Self {
let mut error = Self::from_input(input);
error.kind = ErrorKind::InvalidValue;
error.detail_mut().cause = Some(Box::new(e));
error
}
}