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