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