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 | |
6 | use crate::builder::Command; |
7 | use crate::builder::StyledStr; |
8 | use crate::builder::Styles; |
9 | #[cfg (feature = "error-context" )] |
10 | use crate::error::ContextKind; |
11 | #[cfg (feature = "error-context" )] |
12 | use crate::error::ContextValue; |
13 | use crate::error::ErrorKind; |
14 | use crate::output::TAB; |
15 | |
16 | /// Defines how to format an error for displaying to the user |
17 | pub 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 ] |
29 | pub struct KindFormatter; |
30 | |
31 | impl 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" )] |
55 | pub struct RichFormatter; |
56 | |
57 | #[cfg (feature = "error-context" )] |
58 | impl 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 | |
129 | fn 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" )] |
137 | fn 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" )] |
426 | fn 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 | |
451 | pub(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. |
470 | fn singular_or_plural(n: usize) -> &'static str { |
471 | if n > 1 { |
472 | " were provided" |
473 | } else { |
474 | " was provided" |
475 | } |
476 | } |
477 | |
478 | fn put_usage(styled: &mut StyledStr, usage: &StyledStr) { |
479 | styled.push_str(msg:" \n\n" ); |
480 | styled.push_styled(usage); |
481 | } |
482 | |
483 | pub(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 | |
493 | fn 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" )] |
509 | fn 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 | |
545 | struct Escape<'s>(&'s str); |
546 | |
547 | impl<'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 | |