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