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