clap_builder/error/
format.rs

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
6use crate::builder::Command;
7use crate::builder::StyledStr;
8use crate::builder::Styles;
9#[cfg(feature = "error-context")]
10use crate::error::ContextKind;
11#[cfg(feature = "error-context")]
12use crate::error::ContextValue;
13use crate::error::ErrorKind;
14use crate::output::TAB;
15
16/// Defines how to format an error for displaying to the user
17pub 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]
29pub struct KindFormatter;
30
31impl 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")]
55pub struct RichFormatter;
56
57#[cfg(feature = "error-context")]
58impl 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
129fn 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")]
137fn 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}{} 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")]
426fn 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)) = possible_values {
434        if !possible_values.is_empty() {
435            let _ = write!(styled, "\n{TAB}[{list_name}: ");
436
437            let style = valid.render();
438            let reset = valid.render_reset();
439            for (idx, val) in possible_values.iter().enumerate() {
440                if idx > 0 {
441                    styled.push_str(", ");
442                }
443                let _ = write!(styled, "{style}{}{reset}", Escape(val));
444            }
445
446            styled.push_str("]");
447        }
448    }
449}
450
451pub(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::new();
458    start_error(&mut styled, styles);
459    styled.push_str(message);
460    if let Some(usage) = usage {
461        put_usage(&mut styled, usage);
462    }
463    if let Some(cmd) = cmd {
464        try_help(&mut styled, styles, 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.
470fn singular_or_plural(n: usize) -> &'static str {
471    if n > 1 {
472        " were provided"
473    } else {
474        " was provided"
475    }
476}
477
478fn put_usage(styled: &mut StyledStr, usage: &StyledStr) {
479    styled.push_str("\n\n");
480    styled.push_styled(usage);
481}
482
483pub(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
493fn try_help(styled: &mut StyledStr, styles: &Styles, help: Option<&str>) {
494    if let Some(help) = help {
495        use std::fmt::Write as _;
496        let literal = &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("\n");
505    }
506}
507
508#[cfg(feature = "error-context")]
509fn 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
545struct Escape<'s>(&'s str);
546
547impl<'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}