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 = &error.inner.styles;
35
36 let mut styled = StyledStr::new();
37 start_error(&mut styled, styles);
38 if let Some(msg) = error.kind().as_str() {
39 styled.push_str(msg);
40 } else if let Some(source) = error.inner.source.as_ref() {
41 let _ = write!(styled, "{source}");
42 } else {
43 styled.push_str("unknown cause");
44 }
45 styled.push_str("\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 = &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 invalid_arg = error.get(ContextKind::InvalidArg);
150 let prior_arg = error.get(ContextKind::PriorArg);
151 if let (Some(ContextValue::String(invalid_arg)), Some(prior_arg)) =
152 (invalid_arg, prior_arg)
153 {
154 if ContextValue::String(invalid_arg.clone()) == *prior_arg {
155 let _ = write!(
156 styled,
157 "the argument '{}{invalid_arg}{}' cannot be used multiple times",
158 invalid.render(),
159 invalid.render_reset()
160 );
161 } else {
162 let _ = write!(
163 styled,
164 "the argument '{}{invalid_arg}{}' cannot be used with",
165 invalid.render(),
166 invalid.render_reset()
167 );
168
169 match prior_arg {
170 ContextValue::Strings(values) => {
171 styled.push_str(":");
172 for v in values {
173 let _ = write!(
174 styled,
175 "\n{TAB}{}{v}{}",
176 invalid.render(),
177 invalid.render_reset()
178 );
179 }
180 }
181 ContextValue::String(value) => {
182 let _ = write!(
183 styled,
184 " '{}{value}{}'",
185 invalid.render(),
186 invalid.render_reset()
187 );
188 }
189 _ => {
190 styled.push_str(" one or more of the other specified arguments");
191 }
192 }
193 }
194 true
195 } else {
196 false
197 }
198 }
199 ErrorKind::NoEquals => {
200 let invalid_arg = error.get(ContextKind::InvalidArg);
201 if let Some(ContextValue::String(invalid_arg)) = invalid_arg {
202 let _ = write!(
203 styled,
204 "equal sign is needed when assigning values to '{}{invalid_arg}{}'",
205 invalid.render(),
206 invalid.render_reset()
207 );
208 true
209 } else {
210 false
211 }
212 }
213 ErrorKind::InvalidValue => {
214 let invalid_arg = error.get(ContextKind::InvalidArg);
215 let invalid_value = error.get(ContextKind::InvalidValue);
216 if let (
217 Some(ContextValue::String(invalid_arg)),
218 Some(ContextValue::String(invalid_value)),
219 ) = (invalid_arg, invalid_value)
220 {
221 if invalid_value.is_empty() {
222 let _ = write!(
223 styled,
224 "a value is required for '{}{invalid_arg}{}' but none was supplied",
225 invalid.render(),
226 invalid.render_reset()
227 );
228 } else {
229 let _ = write!(
230 styled,
231 "invalid value '{}{invalid_value}{}' for '{}{invalid_arg}{}'",
232 invalid.render(),
233 invalid.render_reset(),
234 literal.render(),
235 literal.render_reset()
236 );
237 }
238
239 let values = error.get(ContextKind::ValidValue);
240 write_values_list("possible values", styled, valid, values);
241
242 true
243 } else {
244 false
245 }
246 }
247 ErrorKind::InvalidSubcommand => {
248 let invalid_sub = error.get(ContextKind::InvalidSubcommand);
249 if let Some(ContextValue::String(invalid_sub)) = invalid_sub {
250 let _ = write!(
251 styled,
252 "unrecognized subcommand '{}{invalid_sub}{}'",
253 invalid.render(),
254 invalid.render_reset()
255 );
256 true
257 } else {
258 false
259 }
260 }
261 ErrorKind::MissingRequiredArgument => {
262 let invalid_arg = error.get(ContextKind::InvalidArg);
263 if let Some(ContextValue::Strings(invalid_arg)) = invalid_arg {
264 styled.push_str("the following required arguments were not provided:");
265 for v in invalid_arg {
266 let _ = write!(
267 styled,
268 "\n{TAB}{}{v}{}",
269 valid.render(),
270 valid.render_reset()
271 );
272 }
273 true
274 } else {
275 false
276 }
277 }
278 ErrorKind::MissingSubcommand => {
279 let invalid_sub = error.get(ContextKind::InvalidSubcommand);
280 if let Some(ContextValue::String(invalid_sub)) = invalid_sub {
281 let _ = write!(
282 styled,
283 "'{}{invalid_sub}{}' requires a subcommand but one was not provided",
284 invalid.render(),
285 invalid.render_reset()
286 );
287 let values = error.get(ContextKind::ValidSubcommand);
288 write_values_list("subcommands", styled, valid, values);
289
290 true
291 } else {
292 false
293 }
294 }
295 ErrorKind::InvalidUtf8 => false,
296 ErrorKind::TooManyValues => {
297 let invalid_arg = error.get(ContextKind::InvalidArg);
298 let invalid_value = error.get(ContextKind::InvalidValue);
299 if let (
300 Some(ContextValue::String(invalid_arg)),
301 Some(ContextValue::String(invalid_value)),
302 ) = (invalid_arg, invalid_value)
303 {
304 let _ = write!(
305 styled,
306 "unexpected value '{}{invalid_value}{}' for '{}{invalid_arg}{}' found; no more were expected",
307 invalid.render(),
308 invalid.render_reset(),
309 literal.render(),
310 literal.render_reset(),
311 );
312 true
313 } else {
314 false
315 }
316 }
317 ErrorKind::TooFewValues => {
318 let invalid_arg = error.get(ContextKind::InvalidArg);
319 let actual_num_values = error.get(ContextKind::ActualNumValues);
320 let min_values = error.get(ContextKind::MinValues);
321 if let (
322 Some(ContextValue::String(invalid_arg)),
323 Some(ContextValue::Number(actual_num_values)),
324 Some(ContextValue::Number(min_values)),
325 ) = (invalid_arg, actual_num_values, min_values)
326 {
327 let were_provided = singular_or_plural(*actual_num_values as usize);
328 let _ = write!(
329 styled,
330 "{}{min_values}{} more values required by '{}{invalid_arg}{}'; only {}{actual_num_values}{}{were_provided}",
331 valid.render(),
332 valid.render_reset(),
333 literal.render(),
334 literal.render_reset(),
335 invalid.render(),
336 invalid.render_reset(),
337 );
338 true
339 } else {
340 false
341 }
342 }
343 ErrorKind::ValueValidation => {
344 let invalid_arg = error.get(ContextKind::InvalidArg);
345 let invalid_value = error.get(ContextKind::InvalidValue);
346 if let (
347 Some(ContextValue::String(invalid_arg)),
348 Some(ContextValue::String(invalid_value)),
349 ) = (invalid_arg, invalid_value)
350 {
351 let _ = write!(
352 styled,
353 "invalid value '{}{invalid_value}{}' for '{}{invalid_arg}{}'",
354 invalid.render(),
355 invalid.render_reset(),
356 literal.render(),
357 literal.render_reset(),
358 );
359 if let Some(source) = error.inner.source.as_deref() {
360 let _ = write!(styled, ": {source}");
361 }
362 true
363 } else {
364 false
365 }
366 }
367 ErrorKind::WrongNumberOfValues => {
368 let invalid_arg = error.get(ContextKind::InvalidArg);
369 let actual_num_values = error.get(ContextKind::ActualNumValues);
370 let num_values = error.get(ContextKind::ExpectedNumValues);
371 if let (
372 Some(ContextValue::String(invalid_arg)),
373 Some(ContextValue::Number(actual_num_values)),
374 Some(ContextValue::Number(num_values)),
375 ) = (invalid_arg, actual_num_values, num_values)
376 {
377 let were_provided = singular_or_plural(*actual_num_values as usize);
378 let _ = write!(
379 styled,
380 "{}{num_values}{} values required for '{}{invalid_arg}{}' but {}{actual_num_values}{}{were_provided}",
381 valid.render(),
382 valid.render_reset(),
383 literal.render(),
384 literal.render_reset(),
385 invalid.render(),
386 invalid.render_reset(),
387 );
388 true
389 } else {
390 false
391 }
392 }
393 ErrorKind::UnknownArgument => {
394 let invalid_arg = error.get(ContextKind::InvalidArg);
395 if let Some(ContextValue::String(invalid_arg)) = invalid_arg {
396 let _ = write!(
397 styled,
398 "unexpected argument '{}{invalid_arg}{}' found",
399 invalid.render(),
400 invalid.render_reset(),
401 );
402 true
403 } else {
404 false
405 }
406 }
407 ErrorKind::DisplayHelp
408 | ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
409 | ErrorKind::DisplayVersion
410 | ErrorKind::Io
411 | ErrorKind::Format => false,
412 }
413}
414
415#[cfg(feature = "error-context")]
416fn write_values_list(
417 list_name: &'static str,
418 styled: &mut StyledStr,
419 valid: &anstyle::Style,
420 possible_values: Option<&ContextValue>,
421) {
422 use std::fmt::Write as _;
423 if let Some(ContextValue::Strings(possible_values)) = possible_values {
424 if !possible_values.is_empty() {
425 let _ = write!(styled, "\n{TAB}[{list_name}: ");
426
427 let style = valid.render();
428 let reset = valid.render_reset();
429 for (idx, val) in possible_values.iter().enumerate() {
430 if idx > 0 {
431 styled.push_str(", ");
432 }
433 let _ = write!(styled, "{style}{}{reset}", Escape(val));
434 }
435
436 styled.push_str("]");
437 }
438 }
439}
440
441pub(crate) fn format_error_message(
442 message: &str,
443 styles: &Styles,
444 cmd: Option<&Command>,
445 usage: Option<&StyledStr>,
446) -> StyledStr {
447 let mut styled = StyledStr::new();
448 start_error(&mut styled, styles);
449 styled.push_str(message);
450 if let Some(usage) = usage {
451 put_usage(&mut styled, usage);
452 }
453 if let Some(cmd) = cmd {
454 try_help(&mut styled, styles, get_help_flag(cmd));
455 }
456 styled
457}
458
459/// Returns the singular or plural form on the verb to be based on the argument's value.
460fn singular_or_plural(n: usize) -> &'static str {
461 if n > 1 {
462 " were provided"
463 } else {
464 " was provided"
465 }
466}
467
468fn put_usage(styled: &mut StyledStr, usage: &StyledStr) {
469 styled.push_str("\n\n");
470 styled.push_styled(usage);
471}
472
473pub(crate) fn get_help_flag(cmd: &Command) -> Option<&'static str> {
474 if !cmd.is_disable_help_flag_set() {
475 Some("--help")
476 } else if cmd.has_subcommands() && !cmd.is_disable_help_subcommand_set() {
477 Some("help")
478 } else {
479 None
480 }
481}
482
483fn try_help(styled: &mut StyledStr, styles: &Styles, help: Option<&str>) {
484 if let Some(help) = help {
485 use std::fmt::Write as _;
486 let literal = &styles.get_literal();
487 let _ = write!(
488 styled,
489 "\n\nFor more information, try '{}{help}{}'.\n",
490 literal.render(),
491 literal.render_reset()
492 );
493 } else {
494 styled.push_str("\n");
495 }
496}
497
498#[cfg(feature = "error-context")]
499fn did_you_mean(styled: &mut StyledStr, styles: &Styles, context: &str, valid: &ContextValue) {
500 use std::fmt::Write as _;
501
502 let _ = write!(
503 styled,
504 "{TAB}{}tip:{}",
505 styles.get_valid().render(),
506 styles.get_valid().render_reset()
507 );
508 if let ContextValue::String(valid) = valid {
509 let _ = write!(
510 styled,
511 " a similar {context} exists: '{}{valid}{}'",
512 styles.get_valid().render(),
513 styles.get_valid().render_reset()
514 );
515 } else if let ContextValue::Strings(valid) = valid {
516 if valid.len() == 1 {
517 let _ = write!(styled, " a similar {context} exists: ",);
518 } else {
519 let _ = write!(styled, " some similar {context}s exist: ",);
520 }
521 for (i, valid) in valid.iter().enumerate() {
522 if i != 0 {
523 styled.push_str(", ");
524 }
525 let _ = write!(
526 styled,
527 "'{}{valid}{}'",
528 styles.get_valid().render(),
529 styles.get_valid().render_reset()
530 );
531 }
532 }
533}
534
535struct Escape<'s>(&'s str);
536
537impl<'s> std::fmt::Display for Escape<'s> {
538 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
539 if self.0.contains(char::is_whitespace) {
540 std::fmt::Debug::fmt(self.0, f)
541 } else {
542 self.0.fmt(f)
543 }
544 }
545}
546