1#![allow(missing_copy_implementations)]
2#![allow(missing_debug_implementations)]
3#![cfg_attr(not(feature = "error-context"), allow(dead_code))]
4#![cfg_attr(not(feature = "error-context"), allow(unused_imports))]
5
6use crate::builder::Command;
7use crate::builder::StyledStr;
8use crate::builder::Styles;
9#[cfg(feature = "error-context")]
10use crate::error::ContextKind;
11#[cfg(feature = "error-context")]
12use crate::error::ContextValue;
13use crate::error::ErrorKind;
14use crate::output::TAB;
15
16/// Defines how to format an error for displaying to the user
17pub trait ErrorFormatter: Sized {
18 /// Stylize the error for the terminal
19 fn format_error(error: &crate::error::Error<Self>) -> StyledStr;
20}
21
22/// Report [`ErrorKind`]
23///
24/// No context is included.
25///
26/// **NOTE:** Consider removing the `error-context` default feature if using this to remove all
27/// overhead for [`RichFormatter`].
28#[non_exhaustive]
29pub struct KindFormatter;
30
31impl ErrorFormatter for KindFormatter {
32 fn format_error(error: &crate::error::Error<Self>) -> StyledStr {
33 use std::fmt::Write as _;
34 let styles: &Styles = &error.inner.styles;
35
36 let mut styled: StyledStr = StyledStr::new();
37 start_error(&mut styled, styles);
38 if let Some(msg: &str) = error.kind().as_str() {
39 styled.push_str(msg);
40 } else if let Some(source: &Box) = error.inner.source.as_ref() {
41 let _ = write!(styled, "{source}");
42 } else {
43 styled.push_str(msg:"unknown cause");
44 }
45 styled.push_str(msg:"\n");
46 styled
47 }
48}
49
50/// Richly formatted error context
51///
52/// This follows the [rustc diagnostic style guide](https://rustc-dev-guide.rust-lang.org/diagnostics.html#suggestion-style-guide).
53#[non_exhaustive]
54#[cfg(feature = "error-context")]
55pub struct RichFormatter;
56
57#[cfg(feature = "error-context")]
58impl ErrorFormatter for RichFormatter {
59 fn format_error(error: &crate::error::Error<Self>) -> StyledStr {
60 use std::fmt::Write as _;
61 let styles = &error.inner.styles;
62 let valid = &styles.get_valid();
63
64 let mut styled = StyledStr::new();
65 start_error(&mut styled, styles);
66
67 if !write_dynamic_context(error, &mut styled, styles) {
68 if let Some(msg) = error.kind().as_str() {
69 styled.push_str(msg);
70 } else if let Some(source) = error.inner.source.as_ref() {
71 let _ = write!(styled, "{source}");
72 } else {
73 styled.push_str("unknown cause");
74 }
75 }
76
77 let mut suggested = false;
78 if let Some(valid) = error.get(ContextKind::SuggestedSubcommand) {
79 styled.push_str("\n");
80 if !suggested {
81 styled.push_str("\n");
82 suggested = true;
83 }
84 did_you_mean(&mut styled, styles, "subcommand", valid);
85 }
86 if let Some(valid) = error.get(ContextKind::SuggestedArg) {
87 styled.push_str("\n");
88 if !suggested {
89 styled.push_str("\n");
90 suggested = true;
91 }
92 did_you_mean(&mut styled, styles, "argument", valid);
93 }
94 if let Some(valid) = error.get(ContextKind::SuggestedValue) {
95 styled.push_str("\n");
96 if !suggested {
97 styled.push_str("\n");
98 suggested = true;
99 }
100 did_you_mean(&mut styled, styles, "value", valid);
101 }
102 let suggestions = error.get(ContextKind::Suggested);
103 if let Some(ContextValue::StyledStrs(suggestions)) = suggestions {
104 if !suggested {
105 styled.push_str("\n");
106 }
107 for suggestion in suggestions {
108 let _ = write!(
109 styled,
110 "\n{TAB}{}tip:{} ",
111 valid.render(),
112 valid.render_reset()
113 );
114 styled.push_styled(suggestion);
115 }
116 }
117
118 let usage = error.get(ContextKind::Usage);
119 if let Some(ContextValue::StyledStr(usage)) = usage {
120 put_usage(&mut styled, usage);
121 }
122
123 try_help(&mut styled, styles, error.inner.help_flag);
124
125 styled
126 }
127}
128
129fn start_error(styled: &mut StyledStr, styles: &Styles) {
130 use std::fmt::Write as _;
131 let error: &&Style = &styles.get_error();
132 let _ = write!(styled, "{}error:{} ", error.render(), error.render_reset());
133}
134
135#[must_use]
136#[cfg(feature = "error-context")]
137fn write_dynamic_context(
138 error: &crate::error::Error,
139 styled: &mut StyledStr,
140 styles: &Styles,
141) -> bool {
142 use std::fmt::Write as _;
143 let valid = styles.get_valid();
144 let invalid = styles.get_invalid();
145 let literal = styles.get_literal();
146
147 match error.kind() {
148 ErrorKind::ArgumentConflict => {
149 let mut prior_arg = error.get(ContextKind::PriorArg);
150 if let Some(ContextValue::String(invalid_arg)) = error.get(ContextKind::InvalidArg) {
151 if Some(&ContextValue::String(invalid_arg.clone())) == prior_arg {
152 prior_arg = None;
153 let _ = write!(
154 styled,
155 "the argument '{}{invalid_arg}{}' cannot be used multiple times",
156 invalid.render(),
157 invalid.render_reset()
158 );
159 } else {
160 let _ = write!(
161 styled,
162 "the argument '{}{invalid_arg}{}' cannot be used with",
163 invalid.render(),
164 invalid.render_reset()
165 );
166 }
167 } else if let Some(ContextValue::String(invalid_arg)) =
168 error.get(ContextKind::InvalidSubcommand)
169 {
170 let _ = write!(
171 styled,
172 "the subcommand '{}{invalid_arg}{}' cannot be used with",
173 invalid.render(),
174 invalid.render_reset()
175 );
176 } else {
177 styled.push_str(error.kind().as_str().unwrap());
178 }
179
180 if let Some(prior_arg) = prior_arg {
181 match prior_arg {
182 ContextValue::Strings(values) => {
183 styled.push_str(":");
184 for v in values {
185 let _ = write!(
186 styled,
187 "\n{TAB}{}{v}{}",
188 invalid.render(),
189 invalid.render_reset()
190 );
191 }
192 }
193 ContextValue::String(value) => {
194 let _ = write!(
195 styled,
196 " '{}{value}{}'",
197 invalid.render(),
198 invalid.render_reset()
199 );
200 }
201 _ => {
202 styled.push_str(" one or more of the other specified arguments");
203 }
204 }
205 }
206
207 true
208 }
209 ErrorKind::NoEquals => {
210 let invalid_arg = error.get(ContextKind::InvalidArg);
211 if let Some(ContextValue::String(invalid_arg)) = invalid_arg {
212 let _ = write!(
213 styled,
214 "equal sign is needed when assigning values to '{}{invalid_arg}{}'",
215 invalid.render(),
216 invalid.render_reset()
217 );
218 true
219 } else {
220 false
221 }
222 }
223 ErrorKind::InvalidValue => {
224 let invalid_arg = error.get(ContextKind::InvalidArg);
225 let invalid_value = error.get(ContextKind::InvalidValue);
226 if let (
227 Some(ContextValue::String(invalid_arg)),
228 Some(ContextValue::String(invalid_value)),
229 ) = (invalid_arg, invalid_value)
230 {
231 if invalid_value.is_empty() {
232 let _ = write!(
233 styled,
234 "a value is required for '{}{invalid_arg}{}' but none was supplied",
235 invalid.render(),
236 invalid.render_reset()
237 );
238 } else {
239 let _ = write!(
240 styled,
241 "invalid value '{}{invalid_value}{}' for '{}{invalid_arg}{}'",
242 invalid.render(),
243 invalid.render_reset(),
244 literal.render(),
245 literal.render_reset()
246 );
247 }
248
249 let values = error.get(ContextKind::ValidValue);
250 write_values_list("possible values", styled, valid, values);
251
252 true
253 } else {
254 false
255 }
256 }
257 ErrorKind::InvalidSubcommand => {
258 let invalid_sub = error.get(ContextKind::InvalidSubcommand);
259 if let Some(ContextValue::String(invalid_sub)) = invalid_sub {
260 let _ = write!(
261 styled,
262 "unrecognized subcommand '{}{invalid_sub}{}'",
263 invalid.render(),
264 invalid.render_reset()
265 );
266 true
267 } else {
268 false
269 }
270 }
271 ErrorKind::MissingRequiredArgument => {
272 let invalid_arg = error.get(ContextKind::InvalidArg);
273 if let Some(ContextValue::Strings(invalid_arg)) = invalid_arg {
274 styled.push_str("the following required arguments were not provided:");
275 for v in invalid_arg {
276 let _ = write!(
277 styled,
278 "\n{TAB}{}{v}{}",
279 valid.render(),
280 valid.render_reset()
281 );
282 }
283 true
284 } else {
285 false
286 }
287 }
288 ErrorKind::MissingSubcommand => {
289 let invalid_sub = error.get(ContextKind::InvalidSubcommand);
290 if let Some(ContextValue::String(invalid_sub)) = invalid_sub {
291 let _ = write!(
292 styled,
293 "'{}{invalid_sub}{}' requires a subcommand but one was not provided",
294 invalid.render(),
295 invalid.render_reset()
296 );
297 let values = error.get(ContextKind::ValidSubcommand);
298 write_values_list("subcommands", styled, valid, values);
299
300 true
301 } else {
302 false
303 }
304 }
305 ErrorKind::InvalidUtf8 => false,
306 ErrorKind::TooManyValues => {
307 let invalid_arg = error.get(ContextKind::InvalidArg);
308 let invalid_value = error.get(ContextKind::InvalidValue);
309 if let (
310 Some(ContextValue::String(invalid_arg)),
311 Some(ContextValue::String(invalid_value)),
312 ) = (invalid_arg, invalid_value)
313 {
314 let _ = write!(
315 styled,
316 "unexpected value '{}{invalid_value}{}' for '{}{invalid_arg}{}' found; no more were expected",
317 invalid.render(),
318 invalid.render_reset(),
319 literal.render(),
320 literal.render_reset(),
321 );
322 true
323 } else {
324 false
325 }
326 }
327 ErrorKind::TooFewValues => {
328 let invalid_arg = error.get(ContextKind::InvalidArg);
329 let actual_num_values = error.get(ContextKind::ActualNumValues);
330 let min_values = error.get(ContextKind::MinValues);
331 if let (
332 Some(ContextValue::String(invalid_arg)),
333 Some(ContextValue::Number(actual_num_values)),
334 Some(ContextValue::Number(min_values)),
335 ) = (invalid_arg, actual_num_values, min_values)
336 {
337 let were_provided = singular_or_plural(*actual_num_values as usize);
338 let _ = write!(
339 styled,
340 "{}{min_values}{} more values required by '{}{invalid_arg}{}'; only {}{actual_num_values}{}{were_provided}",
341 valid.render(),
342 valid.render_reset(),
343 literal.render(),
344 literal.render_reset(),
345 invalid.render(),
346 invalid.render_reset(),
347 );
348 true
349 } else {
350 false
351 }
352 }
353 ErrorKind::ValueValidation => {
354 let invalid_arg = error.get(ContextKind::InvalidArg);
355 let invalid_value = error.get(ContextKind::InvalidValue);
356 if let (
357 Some(ContextValue::String(invalid_arg)),
358 Some(ContextValue::String(invalid_value)),
359 ) = (invalid_arg, invalid_value)
360 {
361 let _ = write!(
362 styled,
363 "invalid value '{}{invalid_value}{}' for '{}{invalid_arg}{}'",
364 invalid.render(),
365 invalid.render_reset(),
366 literal.render(),
367 literal.render_reset(),
368 );
369 if let Some(source) = error.inner.source.as_deref() {
370 let _ = write!(styled, ": {source}");
371 }
372 true
373 } else {
374 false
375 }
376 }
377 ErrorKind::WrongNumberOfValues => {
378 let invalid_arg = error.get(ContextKind::InvalidArg);
379 let actual_num_values = error.get(ContextKind::ActualNumValues);
380 let num_values = error.get(ContextKind::ExpectedNumValues);
381 if let (
382 Some(ContextValue::String(invalid_arg)),
383 Some(ContextValue::Number(actual_num_values)),
384 Some(ContextValue::Number(num_values)),
385 ) = (invalid_arg, actual_num_values, num_values)
386 {
387 let were_provided = singular_or_plural(*actual_num_values as usize);
388 let _ = write!(
389 styled,
390 "{}{num_values}{} values required for '{}{invalid_arg}{}' but {}{actual_num_values}{}{were_provided}",
391 valid.render(),
392 valid.render_reset(),
393 literal.render(),
394 literal.render_reset(),
395 invalid.render(),
396 invalid.render_reset(),
397 );
398 true
399 } else {
400 false
401 }
402 }
403 ErrorKind::UnknownArgument => {
404 let invalid_arg = error.get(ContextKind::InvalidArg);
405 if let Some(ContextValue::String(invalid_arg)) = invalid_arg {
406 let _ = write!(
407 styled,
408 "unexpected argument '{}{invalid_arg}{}' found",
409 invalid.render(),
410 invalid.render_reset(),
411 );
412 true
413 } else {
414 false
415 }
416 }
417 ErrorKind::DisplayHelp
418 | ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
419 | ErrorKind::DisplayVersion
420 | ErrorKind::Io
421 | ErrorKind::Format => false,
422 }
423}
424
425#[cfg(feature = "error-context")]
426fn write_values_list(
427 list_name: &'static str,
428 styled: &mut StyledStr,
429 valid: &anstyle::Style,
430 possible_values: Option<&ContextValue>,
431) {
432 use std::fmt::Write as _;
433 if let Some(ContextValue::Strings(possible_values: &Vec)) = possible_values {
434 if !possible_values.is_empty() {
435 let _ = write!(styled, "\n{TAB}[{list_name}: ");
436
437 let style: impl Display + Copy + Clone = valid.render();
438 let reset: impl Display + Copy + Clone = valid.render_reset();
439 for (idx: usize, val: &String) in possible_values.iter().enumerate() {
440 if idx > 0 {
441 styled.push_str(msg:", ");
442 }
443 let _ = write!(styled, "{style}{}{reset}", Escape(val));
444 }
445
446 styled.push_str(msg:"]");
447 }
448 }
449}
450
451pub(crate) fn format_error_message(
452 message: &str,
453 styles: &Styles,
454 cmd: Option<&Command>,
455 usage: Option<&StyledStr>,
456) -> StyledStr {
457 let mut styled: StyledStr = StyledStr::new();
458 start_error(&mut styled, styles);
459 styled.push_str(msg:message);
460 if let Some(usage: &StyledStr) = usage {
461 put_usage(&mut styled, usage);
462 }
463 if let Some(cmd: &Command) = cmd {
464 try_help(&mut styled, styles, help:get_help_flag(cmd));
465 }
466 styled
467}
468
469/// Returns the singular or plural form on the verb to be based on the argument's value.
470fn singular_or_plural(n: usize) -> &'static str {
471 if n > 1 {
472 " were provided"
473 } else {
474 " was provided"
475 }
476}
477
478fn put_usage(styled: &mut StyledStr, usage: &StyledStr) {
479 styled.push_str(msg:"\n\n");
480 styled.push_styled(usage);
481}
482
483pub(crate) fn get_help_flag(cmd: &Command) -> Option<&'static str> {
484 if !cmd.is_disable_help_flag_set() {
485 Some("--help")
486 } else if cmd.has_subcommands() && !cmd.is_disable_help_subcommand_set() {
487 Some("help")
488 } else {
489 None
490 }
491}
492
493fn try_help(styled: &mut StyledStr, styles: &Styles, help: Option<&str>) {
494 if let Some(help: &str) = help {
495 use std::fmt::Write as _;
496 let literal: &&Style = &styles.get_literal();
497 let _ = write!(
498 styled,
499 "\n\nFor more information, try '{}{help}{}'.\n",
500 literal.render(),
501 literal.render_reset()
502 );
503 } else {
504 styled.push_str(msg:"\n");
505 }
506}
507
508#[cfg(feature = "error-context")]
509fn did_you_mean(styled: &mut StyledStr, styles: &Styles, context: &str, valid: &ContextValue) {
510 use std::fmt::Write as _;
511
512 let _ = write!(
513 styled,
514 "{TAB}{}tip:{}",
515 styles.get_valid().render(),
516 styles.get_valid().render_reset()
517 );
518 if let ContextValue::String(valid) = valid {
519 let _ = write!(
520 styled,
521 " a similar {context} exists: '{}{valid}{}'",
522 styles.get_valid().render(),
523 styles.get_valid().render_reset()
524 );
525 } else if let ContextValue::Strings(valid) = valid {
526 if valid.len() == 1 {
527 let _ = write!(styled, " a similar {context} exists: ",);
528 } else {
529 let _ = write!(styled, " some similar {context}s exist: ",);
530 }
531 for (i, valid) in valid.iter().enumerate() {
532 if i != 0 {
533 styled.push_str(", ");
534 }
535 let _ = write!(
536 styled,
537 "'{}{valid}{}'",
538 styles.get_valid().render(),
539 styles.get_valid().render_reset()
540 );
541 }
542 }
543}
544
545struct Escape<'s>(&'s str);
546
547impl<'s> std::fmt::Display for Escape<'s> {
548 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
549 if self.0.contains(char::is_whitespace) {
550 std::fmt::Debug::fmt(self.0, f)
551 } else {
552 self.0.fmt(f)
553 }
554 }
555}
556