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 std::borrow::Cow;
7
8use crate::builder::Command;
9use crate::builder::StyledStr;
10use crate::builder::Styles;
11#[cfg(feature = "error-context")]
12use crate::error::ContextKind;
13#[cfg(feature = "error-context")]
14use crate::error::ContextValue;
15use crate::error::ErrorKind;
16use crate::output::TAB;
17use crate::ArgAction;
18
19/// Defines how to format an error for displaying to the user
20pub 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]
36pub struct KindFormatter;
37
38impl ErrorFormatter for KindFormatter {
39    fn format_error(error: &crate::error::Error<Self>) -> StyledStr {
40        use std::fmt::Write as _;
41        let styles = &error.inner.styles;
42
43        let mut styled = StyledStr::new();
44        start_error(&mut styled, styles);
45        if let Some(msg) = error.kind().as_str() {
46            styled.push_str(msg);
47        } else if let Some(source) = error.inner.source.as_ref() {
48            let _ = write!(styled, "{source}");
49        } else {
50            styled.push_str("unknown cause");
51        }
52        styled.push_str("\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")]
62pub struct RichFormatter;
63
64#[cfg(feature = "error-context")]
65impl 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
131fn start_error(styled: &mut StyledStr, styles: &Styles) {
132    use std::fmt::Write as _;
133    let error = &styles.get_error();
134    let _ = write!(styled, "{error}error:{error:#} ");
135}
136
137#[must_use]
138#[cfg(feature = "error-context")]
139fn 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")]
373fn 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)) = possible_values {
381        if !possible_values.is_empty() {
382            let _ = write!(styled, "\n{TAB}[{list_name}: ");
383
384            for (idx, val) in possible_values.iter().enumerate() {
385                if idx > 0 {
386                    styled.push_str(", ");
387                }
388                let _ = write!(styled, "{valid}{}{valid:#}", Escape(val));
389            }
390
391            styled.push_str("]");
392        }
393    }
394}
395
396pub(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::new();
403    start_error(&mut styled, styles);
404    styled.push_str(message);
405    if let Some(usage) = usage {
406        put_usage(&mut styled, usage);
407    }
408    if let Some(cmd) = cmd {
409        try_help(&mut styled, styles, 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.
415fn singular_or_plural(n: usize) -> &'static str {
416    if n > 1 {
417        " were provided"
418    } else {
419        " was provided"
420    }
421}
422
423fn put_usage(styled: &mut StyledStr, usage: &StyledStr) {
424    styled.push_str("\n\n");
425    styled.push_styled(usage);
426}
427
428pub(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) = 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
440fn get_user_help_flag(cmd: &Command) -> Option<String> {
441    let arg = cmd.get_arguments().find(|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    arg.get_long()
452        .map(|long| format!("--{long}"))
453        .or_else(|| arg.get_short().map(|short| format!("-{short}")))
454}
455
456fn try_help(styled: &mut StyledStr, styles: &Styles, help: Option<&str>) {
457    if let Some(help) = help {
458        use std::fmt::Write as _;
459        let literal = &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("\n");
466    }
467}
468
469#[cfg(feature = "error-context")]
470fn did_you_mean(styled: &mut StyledStr, styles: &Styles, context: &str, possibles: &ContextValue) {
471    use std::fmt::Write as _;
472
473    let valid = &styles.get_valid();
474    let _ = write!(styled, "{TAB}{valid}tip:{valid:#}",);
475    if let ContextValue::String(possible) = possibles {
476        let _ = write!(
477            styled,
478            " a similar {context} exists: '{valid}{possible}{valid:#}'",
479        );
480    } else if let ContextValue::Strings(possibles) = 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, possible) in possibles.iter().enumerate() {
487            if i != 0 {
488                styled.push_str(", ");
489            }
490            let _ = write!(styled, "'{valid}{possible}{valid:#}'",);
491        }
492    }
493}
494
495struct Escape<'s>(&'s str);
496
497impl 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}