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