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 root: Option<&'static crate::help::Command>,
75}
76
77#[derive(Debug, Default)]
78struct Detail {
79 request: Option<Request>,
80 token: String,
82 value: Option<String>,
83 cause: Option<BoxError>,
84 path: Vec<&'static str>,
86 similar: Option<(&'static str, String)>,
88}
89
90#[derive(Debug)]
92pub struct Error {
93 kind: ErrorKind,
94 cut: bool,
95 offset: usize,
96 detail: Option<Box<Detail>>,
97}
98
99impl Error {
100 fn with_token(kind: ErrorKind, offset: usize, token: String) -> Self {
101 Self {
102 kind,
103 cut: true,
104 offset,
105 detail: Some(Box::new(Detail {
106 token,
107 ..Detail::default()
108 })),
109 }
110 }
111
112 pub fn unknown_flag(offset: usize, token: impl Into<String>) -> Self {
114 Self::with_token(ErrorKind::UnknownFlag, offset, token.into())
115 }
116
117 pub fn unexpected_arg(offset: usize, token: impl Into<String>) -> Self {
119 Self::with_token(ErrorKind::UnexpectedArg, offset, token.into())
120 }
121
122 pub fn missing_value(offset: usize, flag: impl Into<String>) -> Self {
124 Self::with_token(ErrorKind::MissingValue, offset, flag.into())
125 }
126
127 pub fn unexpected_value(offset: usize, flag: impl Into<String>, value: &[u8]) -> Self {
129 let mut error = Self::with_token(ErrorKind::UnexpectedValue, offset, flag.into());
130 error.detail_mut().value = Some(String::from_utf8_lossy(value).into_owned());
131 error
132 }
133
134 pub fn missing_required(offset: usize, flag: impl Into<String>) -> Self {
136 Self::with_token(ErrorKind::MissingRequired, offset, flag.into())
137 }
138
139 pub fn missing_argument(offset: usize, name: impl Into<String>) -> Self {
141 Self::with_token(ErrorKind::MissingArgument, offset, name.into())
142 }
143
144 pub fn required_by(offset: usize, target: impl Into<String>, name: &str) -> Self {
146 let mut error = Self::with_token(ErrorKind::MissingRequired, offset, target.into());
147 error.detail_mut().value = Some(name.to_owned());
148 error
149 }
150
151 pub fn conflict(offset: usize, name: impl Into<String>, other: &str) -> Self {
153 let mut error = Self::with_token(ErrorKind::Conflict, offset, name.into());
154 error.detail_mut().value = Some(other.to_owned());
155 error
156 }
157
158 pub fn missing_one_of(offset: usize, group: impl Into<String>, members: &[&str]) -> Self {
160 let mut error = Self::with_token(ErrorKind::MissingOneOf, offset, group.into());
161 error.detail_mut().value = Some(members.join(", "));
162 error
163 }
164
165 pub fn requires_double_dash(offset: usize, name: impl Into<String>) -> Self {
167 Self::with_token(ErrorKind::RequiresDoubleDash, offset, name.into())
168 }
169
170 fn request(
171 kind: ErrorKind,
172 command: &'static crate::help::Command,
173 long: bool,
174 bare: bool,
175 ) -> Self {
176 Self {
177 kind,
178 cut: true,
179 offset: 0,
180 detail: Some(Box::new(Detail {
181 request: Some(Request {
182 command,
183 long,
184 bare,
185 path: Vec::new(),
186 root: None,
187 }),
188 ..Detail::default()
189 })),
190 }
191 }
192
193 pub fn help(command: &'static crate::help::Command, long: bool) -> Self {
195 Self::request(ErrorKind::HelpRequested, command, long, false)
196 }
197
198 pub fn bare_help(command: &'static crate::help::Command) -> Self {
200 Self::request(ErrorKind::HelpRequested, command, false, true)
201 }
202
203 pub fn version(command: &'static crate::help::Command) -> Self {
205 Self::request(ErrorKind::VersionRequested, command, false, false)
206 }
207
208 #[cold]
211 #[inline(never)]
212 pub fn within(mut self, name: &'static str) -> Self {
213 if let Some(detail) = self.detail.as_deref_mut() {
214 match detail.request.as_mut() {
215 Some(request) => request.path.insert(0, name),
216 None => detail.path.insert(0, name),
217 }
218 }
219 self
220 }
221
222 #[cold]
230 #[inline(never)]
231 pub fn suggested(mut self, root: &'static crate::help::Command) -> Self {
232 let Some(detail) = self.detail.as_deref_mut() else {
233 return self;
234 };
235 if let Some(request) = detail.request.as_mut() {
236 request.root = Some(root);
237 return self;
238 }
239 let found = match self.kind {
240 ErrorKind::UnknownFlag => {
241 suggest::flag(root, &detail.path, &detail.token).map(|flag| ("flag", flag))
242 }
243 ErrorKind::UnexpectedArg => suggest::subcommand(root, &detail.path, &detail.token)
244 .map(|name| ("subcommand", name.to_owned())),
245 _ => None,
246 };
247 if let Some(found) = found {
248 detail.similar = Some(found);
249 }
250 self
251 }
252
253 pub fn similar(&self) -> Option<&str> {
256 let (_, similar) = self.detail.as_deref()?.similar.as_ref()?;
257 Some(similar)
258 }
259
260 pub fn render_help(&self, program: &str) -> Option<String> {
263 self.render_help_styled(program, Style::PLAIN)
264 }
265
266 pub fn version_text(&self, program: &str) -> Option<String> {
268 let request = self.detail.as_deref()?.request.as_ref()?;
269 let version = request.command.version?;
270 (self.kind == ErrorKind::VersionRequested).then(|| format!("{program} {version}"))
271 }
272
273 pub fn is_bare_help(&self) -> bool {
275 self.detail
276 .as_deref()
277 .and_then(|d| d.request.as_ref())
278 .is_some_and(|r| r.bare)
279 }
280
281 pub fn missing_subcommand(offset: usize) -> Self {
283 Self {
284 kind: ErrorKind::MissingSubcommand,
285 cut: true,
286 offset,
287 detail: None,
288 }
289 }
290
291 pub fn invalid_value(
293 offset: usize,
294 flag: impl Into<String>,
295 value: &[u8],
296 cause: impl Into<BoxError>,
297 ) -> Self {
298 let cause = cause.into();
299 let choices = cause.downcast_ref::<crate::value::ChoiceError>();
300 let kind = match choices {
301 Some(_) => ErrorKind::InvalidChoice,
302 None => ErrorKind::InvalidValue,
303 };
304 let value = String::from_utf8_lossy(value).into_owned();
305 let similar = choices
306 .and_then(|error| suggest::nearest(&value, error.choices.iter().copied()))
307 .map(|choice| ("value", choice.to_owned()));
308 let mut error = Self::with_token(kind, offset, flag.into());
309 let detail = error.detail_mut();
310 detail.value = Some(value);
311 detail.cause = Some(cause);
312 detail.similar = similar;
313 error
314 }
315
316 fn detail_mut(&mut self) -> &mut Detail {
317 self.detail.get_or_insert_with(Default::default)
318 }
319
320 pub fn kind(&self) -> ErrorKind {
322 self.kind
323 }
324
325 pub fn offset(&self) -> usize {
327 self.offset
328 }
329
330 pub fn token(&self) -> Option<&str> {
332 self.detail.as_deref().map(|d| d.token.as_str())
333 }
334
335 pub fn value(&self) -> Option<&str> {
337 self.detail.as_deref()?.value.as_deref()
338 }
339
340 pub fn is_cut(&self) -> bool {
342 self.cut
343 }
344}
345
346impl PartialEq for Error {
347 fn eq(&self, other: &Self) -> bool {
349 self.kind == other.kind
350 && self.offset == other.offset
351 && self.token() == other.token()
352 && self.value() == other.value()
353 }
354}
355
356impl fmt::Display for Error {
357 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
358 f.write_str(&self.message(Style::PLAIN))
359 }
360}
361
362impl Error {
363 pub fn message(&self, style: Style) -> String {
366 let bad = |text: &str| Style::paint(style.invalid(), text);
367 let good = |text: &str| Style::paint(style.valid(), text);
368 let token = self.token().unwrap_or_default();
369 let value = self.value().unwrap_or_default();
370 match self.kind {
371 ErrorKind::Backtrack => "invalid command line".to_owned(),
372 ErrorKind::UnknownFlag => format!("unknown flag `{}`", bad(token)),
373 ErrorKind::UnexpectedArg => format!("unexpected argument `{}`", bad(token)),
374 ErrorKind::MissingValue => format!("`{}` needs a value", good(token)),
375 ErrorKind::UnexpectedValue => format!(
376 "`{}` does not take a value, got `{}`",
377 bad(token),
378 bad(value)
379 ),
380 ErrorKind::MissingRequired => match self.value() {
381 Some(by) => format!("`{}` is required by `{}`", good(token), bad(by)),
382 None => format!("`{}` is required", good(token)),
383 },
384 ErrorKind::Conflict => format!("`{}` cannot be used with `{}`", bad(token), bad(value)),
385 ErrorKind::HelpRequested => "help requested".to_owned(),
386 ErrorKind::VersionRequested => "version requested".to_owned(),
387 ErrorKind::RequiresDoubleDash => {
388 format!("`{}` can only be set after a `--` separator", bad(token))
389 }
390 ErrorKind::MissingOneOf => format!("one of {} is required ({token})", good(value)),
391 ErrorKind::MissingArgument => format!("missing argument `{}`", good(token)),
392 ErrorKind::MissingSubcommand => "a subcommand is required".to_owned(),
393 ErrorKind::InvalidValue | ErrorKind::InvalidChoice => {
394 let mut text = format!("invalid value `{}` for `{token}`", bad(value));
395 if let Some(cause) = self.detail.as_deref().and_then(|d| d.cause.as_ref()) {
396 text.push_str(": ");
397 text.push_str(&cause.to_string());
398 }
399 text
400 }
401 }
402 }
403
404 pub fn tip(&self, style: Style) -> Option<String> {
407 let (what, similar) = self.detail.as_deref()?.similar.as_ref()?;
408 Some(format!(
409 "{} a similar {what} exists: `{}`",
410 Style::paint(style.valid(), "tip:"),
411 Style::paint(style.valid(), similar),
412 ))
413 }
414
415 pub fn render(&self, style: Style) -> String {
419 let tip = self.tip(style).map(|tip| format!("\n\n {tip}"));
420 format!(
421 "{} {}{}\n\nFor more information, try '{}'.",
422 Style::paint(style.error(), "error:"),
423 self.message(style),
424 tip.unwrap_or_default(),
425 Style::paint(style.flag(), "--help"),
426 )
427 }
428
429 pub fn render_help_styled(&self, program: &str, style: Style) -> Option<String> {
431 let request = self.detail.as_deref()?.request.as_ref()?;
432 if self.kind != ErrorKind::HelpRequested {
433 return None;
434 }
435 let mut path = vec![program];
436 path.extend(&request.path);
437 let width = crate::help::width();
438 let inherited = request
439 .root
440 .map(|root| suggest::inherited(root, &request.path, request.command))
441 .unwrap_or_default();
442 Some(crate::help::render_page(
443 request.command,
444 &path,
445 request.long,
446 width,
447 style,
448 &inherited,
449 ))
450 }
451}
452
453impl std::error::Error for Error {
454 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
455 let cause = self.detail.as_deref()?.cause.as_ref()?;
456 Some(&**cause as &(dyn std::error::Error + 'static))
457 }
458}
459
460impl<'i> ParserError<Argv<'i>> for Error {
461 type Inner = Self;
462
463 #[inline]
464 fn from_input(input: &Argv<'i>) -> Self {
465 Self {
466 kind: ErrorKind::Backtrack,
467 cut: false,
468 offset: input.offset(),
469 detail: None,
470 }
471 }
472
473 #[inline]
476 fn or(self, other: Self) -> Self {
477 if other.kind == ErrorKind::Backtrack && self.kind != ErrorKind::Backtrack {
478 self
479 } else {
480 other
481 }
482 }
483
484 #[inline(always)]
485 fn is_backtrack(&self) -> bool {
486 !self.cut
487 }
488
489 #[inline(always)]
490 fn into_inner(self) -> Result<Self::Inner, Self> {
491 Ok(self)
492 }
493}
494
495impl ModalError for Error {
496 #[inline(always)]
497 fn cut(mut self) -> Self {
498 self.cut = true;
499 self
500 }
501
502 #[inline(always)]
503 fn backtrack(mut self) -> Self {
504 self.cut = false;
505 self
506 }
507}
508
509impl<'i, E> FromExternalError<Argv<'i>, E> for Error
513where
514 E: std::error::Error + Send + Sync + 'static,
515{
516 fn from_external_error(input: &Argv<'i>, e: E) -> Self {
517 let mut error = Self::from_input(input);
518 error.kind = ErrorKind::InvalidValue;
519 error.detail_mut().cause = Some(Box::new(e));
520 error
521 }
522}