1use std::fmt;
15
16use winnow::error::{FromExternalError, ModalError, ParserError};
17
18use crate::help::Style;
19use crate::stream::Argv;
20
21mod suggest;
22
23pub type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
25
26#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
28#[non_exhaustive]
29pub enum ErrorKind {
30 Backtrack,
32 UnknownFlag,
34 UnexpectedArg,
36 MissingValue,
38 UnexpectedValue,
40 MissingRequired,
42 MissingArgument,
44 MissingSubcommand,
46 Conflict,
48 MissingOneOf,
50 RequiresDoubleDash,
52 HelpRequested,
55 VersionRequested,
57 InvalidValue,
59 InvalidChoice,
61}
62
63#[derive(Debug)]
65struct Request {
66 command: &'static crate::help::Command,
67 long: bool,
68 bare: bool,
70 path: Vec<&'static str>,
72}
73
74#[derive(Debug, Default)]
75struct Detail {
76 request: Option<Request>,
77 token: String,
79 value: Option<String>,
80 cause: Option<BoxError>,
81 path: Vec<&'static str>,
83 similar: Option<(&'static str, String)>,
85}
86
87#[derive(Debug)]
89pub struct Error {
90 kind: ErrorKind,
91 cut: bool,
92 offset: usize,
93 detail: Option<Box<Detail>>,
94}
95
96impl Error {
97 fn with_token(kind: ErrorKind, offset: usize, token: String) -> Self {
98 Self {
99 kind,
100 cut: true,
101 offset,
102 detail: Some(Box::new(Detail {
103 token,
104 ..Detail::default()
105 })),
106 }
107 }
108
109 pub fn unknown_flag(offset: usize, token: impl Into<String>) -> Self {
111 Self::with_token(ErrorKind::UnknownFlag, offset, token.into())
112 }
113
114 pub fn unexpected_arg(offset: usize, token: impl Into<String>) -> Self {
116 Self::with_token(ErrorKind::UnexpectedArg, offset, token.into())
117 }
118
119 pub fn missing_value(offset: usize, flag: impl Into<String>) -> Self {
121 Self::with_token(ErrorKind::MissingValue, offset, flag.into())
122 }
123
124 pub fn unexpected_value(offset: usize, flag: impl Into<String>, value: &[u8]) -> Self {
126 let mut error = Self::with_token(ErrorKind::UnexpectedValue, offset, flag.into());
127 error.detail_mut().value = Some(String::from_utf8_lossy(value).into_owned());
128 error
129 }
130
131 pub fn missing_required(offset: usize, flag: impl Into<String>) -> Self {
133 Self::with_token(ErrorKind::MissingRequired, offset, flag.into())
134 }
135
136 pub fn missing_argument(offset: usize, name: impl Into<String>) -> Self {
138 Self::with_token(ErrorKind::MissingArgument, offset, name.into())
139 }
140
141 pub fn required_by(offset: usize, target: impl Into<String>, name: &str) -> Self {
143 let mut error = Self::with_token(ErrorKind::MissingRequired, offset, target.into());
144 error.detail_mut().value = Some(name.to_owned());
145 error
146 }
147
148 pub fn conflict(offset: usize, name: impl Into<String>, other: &str) -> Self {
150 let mut error = Self::with_token(ErrorKind::Conflict, offset, name.into());
151 error.detail_mut().value = Some(other.to_owned());
152 error
153 }
154
155 pub fn missing_one_of(offset: usize, group: impl Into<String>, members: &[&str]) -> Self {
157 let mut error = Self::with_token(ErrorKind::MissingOneOf, offset, group.into());
158 error.detail_mut().value = Some(members.join(", "));
159 error
160 }
161
162 pub fn requires_double_dash(offset: usize, name: impl Into<String>) -> Self {
164 Self::with_token(ErrorKind::RequiresDoubleDash, offset, name.into())
165 }
166
167 fn request(
168 kind: ErrorKind,
169 command: &'static crate::help::Command,
170 long: bool,
171 bare: bool,
172 ) -> Self {
173 Self {
174 kind,
175 cut: true,
176 offset: 0,
177 detail: Some(Box::new(Detail {
178 request: Some(Request {
179 command,
180 long,
181 bare,
182 path: Vec::new(),
183 }),
184 ..Detail::default()
185 })),
186 }
187 }
188
189 pub fn help(command: &'static crate::help::Command, long: bool) -> Self {
191 Self::request(ErrorKind::HelpRequested, command, long, false)
192 }
193
194 pub fn bare_help(command: &'static crate::help::Command) -> Self {
196 Self::request(ErrorKind::HelpRequested, command, false, true)
197 }
198
199 pub fn version(command: &'static crate::help::Command) -> Self {
201 Self::request(ErrorKind::VersionRequested, command, false, false)
202 }
203
204 #[cold]
207 #[inline(never)]
208 pub fn within(mut self, name: &'static str) -> Self {
209 if let Some(detail) = self.detail.as_deref_mut() {
210 match detail.request.as_mut() {
211 Some(request) => request.path.insert(0, name),
212 None => detail.path.insert(0, name),
213 }
214 }
215 self
216 }
217
218 #[cold]
224 #[inline(never)]
225 pub fn suggested(mut self, root: &'static crate::help::Command) -> Self {
226 let Some(detail) = self.detail.as_deref_mut() else {
227 return self;
228 };
229 let found = match self.kind {
230 ErrorKind::UnknownFlag => {
231 suggest::flag(root, &detail.path, &detail.token).map(|flag| ("flag", flag))
232 }
233 ErrorKind::UnexpectedArg => suggest::subcommand(root, &detail.path, &detail.token)
234 .map(|name| ("subcommand", name.to_owned())),
235 _ => None,
236 };
237 if let Some(found) = found {
238 detail.similar = Some(found);
239 }
240 self
241 }
242
243 pub fn similar(&self) -> Option<&str> {
246 let (_, similar) = self.detail.as_deref()?.similar.as_ref()?;
247 Some(similar)
248 }
249
250 pub fn render_help(&self, program: &str) -> Option<String> {
253 self.render_help_styled(program, Style::PLAIN)
254 }
255
256 pub fn version_text(&self, program: &str) -> Option<String> {
258 let request = self.detail.as_deref()?.request.as_ref()?;
259 let version = request.command.version?;
260 (self.kind == ErrorKind::VersionRequested).then(|| format!("{program} {version}"))
261 }
262
263 pub fn is_bare_help(&self) -> bool {
265 self.detail
266 .as_deref()
267 .and_then(|d| d.request.as_ref())
268 .is_some_and(|r| r.bare)
269 }
270
271 pub fn missing_subcommand(offset: usize) -> Self {
273 Self {
274 kind: ErrorKind::MissingSubcommand,
275 cut: true,
276 offset,
277 detail: None,
278 }
279 }
280
281 pub fn invalid_value(
283 offset: usize,
284 flag: impl Into<String>,
285 value: &[u8],
286 cause: impl Into<BoxError>,
287 ) -> Self {
288 let cause = cause.into();
289 let choices = cause.downcast_ref::<crate::value::ChoiceError>();
290 let kind = match choices {
291 Some(_) => ErrorKind::InvalidChoice,
292 None => ErrorKind::InvalidValue,
293 };
294 let value = String::from_utf8_lossy(value).into_owned();
295 let similar = choices
296 .and_then(|error| suggest::nearest(&value, error.choices.iter().copied()))
297 .map(|choice| ("value", choice.to_owned()));
298 let mut error = Self::with_token(kind, offset, flag.into());
299 let detail = error.detail_mut();
300 detail.value = Some(value);
301 detail.cause = Some(cause);
302 detail.similar = similar;
303 error
304 }
305
306 fn detail_mut(&mut self) -> &mut Detail {
307 self.detail.get_or_insert_with(Default::default)
308 }
309
310 pub fn kind(&self) -> ErrorKind {
312 self.kind
313 }
314
315 pub fn offset(&self) -> usize {
317 self.offset
318 }
319
320 pub fn token(&self) -> Option<&str> {
322 self.detail.as_deref().map(|d| d.token.as_str())
323 }
324
325 pub fn value(&self) -> Option<&str> {
327 self.detail.as_deref()?.value.as_deref()
328 }
329
330 pub fn is_cut(&self) -> bool {
332 self.cut
333 }
334}
335
336impl PartialEq for Error {
337 fn eq(&self, other: &Self) -> bool {
339 self.kind == other.kind
340 && self.offset == other.offset
341 && self.token() == other.token()
342 && self.value() == other.value()
343 }
344}
345
346impl fmt::Display for Error {
347 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
348 f.write_str(&self.message(Style::PLAIN))
349 }
350}
351
352impl Error {
353 pub fn message(&self, style: Style) -> String {
356 let bad = |text: &str| Style::paint(style.invalid(), text);
357 let good = |text: &str| Style::paint(style.valid(), text);
358 let token = self.token().unwrap_or_default();
359 let value = self.value().unwrap_or_default();
360 match self.kind {
361 ErrorKind::Backtrack => "invalid command line".to_owned(),
362 ErrorKind::UnknownFlag => format!("unknown flag `{}`", bad(token)),
363 ErrorKind::UnexpectedArg => format!("unexpected argument `{}`", bad(token)),
364 ErrorKind::MissingValue => format!("`{}` needs a value", good(token)),
365 ErrorKind::UnexpectedValue => format!(
366 "`{}` does not take a value, got `{}`",
367 bad(token),
368 bad(value)
369 ),
370 ErrorKind::MissingRequired => match self.value() {
371 Some(by) => format!("`{}` is required by `{}`", good(token), bad(by)),
372 None => format!("`{}` is required", good(token)),
373 },
374 ErrorKind::Conflict => format!("`{}` cannot be used with `{}`", bad(token), bad(value)),
375 ErrorKind::HelpRequested => "help requested".to_owned(),
376 ErrorKind::VersionRequested => "version requested".to_owned(),
377 ErrorKind::RequiresDoubleDash => {
378 format!("`{}` can only be set after a `--` separator", bad(token))
379 }
380 ErrorKind::MissingOneOf => format!("one of {} is required ({token})", good(value)),
381 ErrorKind::MissingArgument => format!("missing argument `{}`", good(token)),
382 ErrorKind::MissingSubcommand => "a subcommand is required".to_owned(),
383 ErrorKind::InvalidValue | ErrorKind::InvalidChoice => {
384 let mut text = format!("invalid value `{}` for `{token}`", bad(value));
385 if let Some(cause) = self.detail.as_deref().and_then(|d| d.cause.as_ref()) {
386 text.push_str(": ");
387 text.push_str(&cause.to_string());
388 }
389 text
390 }
391 }
392 }
393
394 pub fn tip(&self, style: Style) -> Option<String> {
397 let (what, similar) = self.detail.as_deref()?.similar.as_ref()?;
398 Some(format!(
399 "{} a similar {what} exists: `{}`",
400 Style::paint(style.valid(), "tip:"),
401 Style::paint(style.valid(), similar),
402 ))
403 }
404
405 pub fn render(&self, style: Style) -> String {
409 let tip = self.tip(style).map(|tip| format!("\n\n {tip}"));
410 format!(
411 "{} {}{}\n\nFor more information, try '{}'.",
412 Style::paint(style.error(), "error:"),
413 self.message(style),
414 tip.unwrap_or_default(),
415 Style::paint(style.flag(), "--help"),
416 )
417 }
418
419 pub fn render_help_styled(&self, program: &str, style: Style) -> Option<String> {
421 let request = self.detail.as_deref()?.request.as_ref()?;
422 if self.kind != ErrorKind::HelpRequested {
423 return None;
424 }
425 let mut path = vec![program];
426 path.extend(&request.path);
427 let width = crate::help::width();
428 Some(crate::help::render_styled(
429 request.command,
430 &path,
431 request.long,
432 width,
433 style,
434 ))
435 }
436}
437
438impl std::error::Error for Error {
439 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
440 let cause = self.detail.as_deref()?.cause.as_ref()?;
441 Some(&**cause as &(dyn std::error::Error + 'static))
442 }
443}
444
445impl<'i> ParserError<Argv<'i>> for Error {
446 type Inner = Self;
447
448 #[inline]
449 fn from_input(input: &Argv<'i>) -> Self {
450 Self {
451 kind: ErrorKind::Backtrack,
452 cut: false,
453 offset: input.offset(),
454 detail: None,
455 }
456 }
457
458 #[inline]
461 fn or(self, other: Self) -> Self {
462 if other.kind == ErrorKind::Backtrack && self.kind != ErrorKind::Backtrack {
463 self
464 } else {
465 other
466 }
467 }
468
469 #[inline(always)]
470 fn is_backtrack(&self) -> bool {
471 !self.cut
472 }
473
474 #[inline(always)]
475 fn into_inner(self) -> Result<Self::Inner, Self> {
476 Ok(self)
477 }
478}
479
480impl ModalError for Error {
481 #[inline(always)]
482 fn cut(mut self) -> Self {
483 self.cut = true;
484 self
485 }
486
487 #[inline(always)]
488 fn backtrack(mut self) -> Self {
489 self.cut = false;
490 self
491 }
492}
493
494impl<'i, E> FromExternalError<Argv<'i>, E> for Error
498where
499 E: std::error::Error + Send + Sync + 'static,
500{
501 fn from_external_error(input: &Argv<'i>, e: E) -> Self {
502 let mut error = Self::from_input(input);
503 error.kind = ErrorKind::InvalidValue;
504 error.detail_mut().cause = Some(Box::new(e));
505 error
506 }
507}