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 = &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" )] |
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 = &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 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" )] |
416 | fn 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 | |
441 | pub(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. |
460 | fn singular_or_plural(n: usize) -> &'static str { |
461 | if n > 1 { |
462 | " were provided" |
463 | } else { |
464 | " was provided" |
465 | } |
466 | } |
467 | |
468 | fn put_usage(styled: &mut StyledStr, usage: &StyledStr) { |
469 | styled.push_str(" \n\n" ); |
470 | styled.push_styled(usage); |
471 | } |
472 | |
473 | pub(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 | |
483 | fn 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" )] |
499 | fn 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 | |
535 | struct Escape<'s>(&'s str); |
536 | |
537 | impl<'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 | |