clap_builder_cryo/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 invalid_arg = error.get(ContextKind::InvalidArg);
150            let prior_arg = error.get(ContextKind::PriorArg);
151            if let (Some(ContextValue::String(invalid_arg)), Some(prior_arg)) =
152                (invalid_arg, prior_arg)
153            {
154                if ContextValue::String(invalid_arg.clone()) == *prior_arg {
155                    let _ = write!(
156                        styled,
157                        "the argument '{}{invalid_arg}{}' cannot be used multiple times",
158                        invalid.render(),
159                        invalid.render_reset()
160                    );
161                } else {
162                    let _ = write!(
163                        styled,
164                        "the argument '{}{invalid_arg}{}' cannot be used with",
165                        invalid.render(),
166                        invalid.render_reset()
167                    );
168
169                    match prior_arg {
170                        ContextValue::Strings(values) => {
171                            styled.push_str(":");
172                            for v in values {
173                                let _ = write!(
174                                    styled,
175                                    "\n{TAB}{}{v}{}",
176                                    invalid.render(),
177                                    invalid.render_reset()
178                                );
179                            }
180                        }
181                        ContextValue::String(value) => {
182                            let _ = write!(
183                                styled,
184                                " '{}{value}{}'",
185                                invalid.render(),
186                                invalid.render_reset()
187                            );
188                        }
189                        _ => {
190                            styled.push_str(" one or more of the other specified arguments");
191                        }
192                    }
193                }
194                true
195            } else {
196                false
197            }
198        }
199        ErrorKind::NoEquals => {
200            let invalid_arg = error.get(ContextKind::InvalidArg);
201            if let Some(ContextValue::String(invalid_arg)) = invalid_arg {
202                let _ = write!(
203                    styled,
204                    "equal sign is needed when assigning values to '{}{invalid_arg}{}'",
205                    invalid.render(),
206                    invalid.render_reset()
207                );
208                true
209            } else {
210                false
211            }
212        }
213        ErrorKind::InvalidValue => {
214            let invalid_arg = error.get(ContextKind::InvalidArg);
215            let invalid_value = error.get(ContextKind::InvalidValue);
216            if let (
217                Some(ContextValue::String(invalid_arg)),
218                Some(ContextValue::String(invalid_value)),
219            ) = (invalid_arg, invalid_value)
220            {
221                if invalid_value.is_empty() {
222                    let _ = write!(
223                        styled,
224                        "a value is required for '{}{invalid_arg}{}' but none was supplied",
225                        invalid.render(),
226                        invalid.render_reset()
227                    );
228                } else {
229                    let _ = write!(
230                        styled,
231                        "invalid value '{}{invalid_value}{}' for '{}{invalid_arg}{}'",
232                        invalid.render(),
233                        invalid.render_reset(),
234                        literal.render(),
235                        literal.render_reset()
236                    );
237                }
238
239                let values = error.get(ContextKind::ValidValue);
240                write_values_list("possible values", styled, valid, values);
241
242                true
243            } else {
244                false
245            }
246        }
247        ErrorKind::InvalidSubcommand => {
248            let invalid_sub = error.get(ContextKind::InvalidSubcommand);
249            if let Some(ContextValue::String(invalid_sub)) = invalid_sub {
250                let _ = write!(
251                    styled,
252                    "unrecognized subcommand '{}{invalid_sub}{}'",
253                    invalid.render(),
254                    invalid.render_reset()
255                );
256                true
257            } else {
258                false
259            }
260        }
261        ErrorKind::MissingRequiredArgument => {
262            let invalid_arg = error.get(ContextKind::InvalidArg);
263            if let Some(ContextValue::Strings(invalid_arg)) = invalid_arg {
264                styled.push_str("the following required arguments were not provided:");
265                for v in invalid_arg {
266                    let _ = write!(
267                        styled,
268                        "\n{TAB}{}{v}{}",
269                        valid.render(),
270                        valid.render_reset()
271                    );
272                }
273                true
274            } else {
275                false
276            }
277        }
278        ErrorKind::MissingSubcommand => {
279            let invalid_sub = error.get(ContextKind::InvalidSubcommand);
280            if let Some(ContextValue::String(invalid_sub)) = invalid_sub {
281                let _ = write!(
282                    styled,
283                    "'{}{invalid_sub}{}' requires a subcommand but one was not provided",
284                    invalid.render(),
285                    invalid.render_reset()
286                );
287                let values = error.get(ContextKind::ValidSubcommand);
288                write_values_list("subcommands", styled, valid, values);
289
290                true
291            } else {
292                false
293            }
294        }
295        ErrorKind::InvalidUtf8 => false,
296        ErrorKind::TooManyValues => {
297            let invalid_arg = error.get(ContextKind::InvalidArg);
298            let invalid_value = error.get(ContextKind::InvalidValue);
299            if let (
300                Some(ContextValue::String(invalid_arg)),
301                Some(ContextValue::String(invalid_value)),
302            ) = (invalid_arg, invalid_value)
303            {
304                let _ = write!(
305                    styled,
306                    "unexpected value '{}{invalid_value}{}' for '{}{invalid_arg}{}' found; no more were expected",
307                    invalid.render(),
308                    invalid.render_reset(),
309                    literal.render(),
310                    literal.render_reset(),
311                );
312                true
313            } else {
314                false
315            }
316        }
317        ErrorKind::TooFewValues => {
318            let invalid_arg = error.get(ContextKind::InvalidArg);
319            let actual_num_values = error.get(ContextKind::ActualNumValues);
320            let min_values = error.get(ContextKind::MinValues);
321            if let (
322                Some(ContextValue::String(invalid_arg)),
323                Some(ContextValue::Number(actual_num_values)),
324                Some(ContextValue::Number(min_values)),
325            ) = (invalid_arg, actual_num_values, min_values)
326            {
327                let were_provided = singular_or_plural(*actual_num_values as usize);
328                let _ = write!(
329                    styled,
330                    "{}{min_values}{} more values required by '{}{invalid_arg}{}'; only {}{actual_num_values}{}{were_provided}",
331                    valid.render(),
332                    valid.render_reset(),
333                    literal.render(),
334                    literal.render_reset(),
335                    invalid.render(),
336                    invalid.render_reset(),
337                );
338                true
339            } else {
340                false
341            }
342        }
343        ErrorKind::ValueValidation => {
344            let invalid_arg = error.get(ContextKind::InvalidArg);
345            let invalid_value = error.get(ContextKind::InvalidValue);
346            if let (
347                Some(ContextValue::String(invalid_arg)),
348                Some(ContextValue::String(invalid_value)),
349            ) = (invalid_arg, invalid_value)
350            {
351                let _ = write!(
352                    styled,
353                    "invalid value '{}{invalid_value}{}' for '{}{invalid_arg}{}'",
354                    invalid.render(),
355                    invalid.render_reset(),
356                    literal.render(),
357                    literal.render_reset(),
358                );
359                if let Some(source) = error.inner.source.as_deref() {
360                    let _ = write!(styled, ": {source}");
361                }
362                true
363            } else {
364                false
365            }
366        }
367        ErrorKind::WrongNumberOfValues => {
368            let invalid_arg = error.get(ContextKind::InvalidArg);
369            let actual_num_values = error.get(ContextKind::ActualNumValues);
370            let num_values = error.get(ContextKind::ExpectedNumValues);
371            if let (
372                Some(ContextValue::String(invalid_arg)),
373                Some(ContextValue::Number(actual_num_values)),
374                Some(ContextValue::Number(num_values)),
375            ) = (invalid_arg, actual_num_values, num_values)
376            {
377                let were_provided = singular_or_plural(*actual_num_values as usize);
378                let _ = write!(
379                    styled,
380                    "{}{num_values}{} values required for '{}{invalid_arg}{}' but {}{actual_num_values}{}{were_provided}",
381                    valid.render(),
382                    valid.render_reset(),
383                    literal.render(),
384                    literal.render_reset(),
385                    invalid.render(),
386                    invalid.render_reset(),
387                );
388                true
389            } else {
390                false
391            }
392        }
393        ErrorKind::UnknownArgument => {
394            let invalid_arg = error.get(ContextKind::InvalidArg);
395            if let Some(ContextValue::String(invalid_arg)) = invalid_arg {
396                let _ = write!(
397                    styled,
398                    "unexpected argument '{}{invalid_arg}{}' found",
399                    invalid.render(),
400                    invalid.render_reset(),
401                );
402                true
403            } else {
404                false
405            }
406        }
407        ErrorKind::DisplayHelp
408        | ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
409        | ErrorKind::DisplayVersion
410        | ErrorKind::Io
411        | ErrorKind::Format => false,
412    }
413}
414
415#[cfg(feature = "error-context")]
416fn write_values_list(
417    list_name: &'static str,
418    styled: &mut StyledStr,
419    valid: &anstyle::Style,
420    possible_values: Option<&ContextValue>,
421) {
422    use std::fmt::Write as _;
423    if let Some(ContextValue::Strings(possible_values)) = possible_values {
424        if !possible_values.is_empty() {
425            let _ = write!(styled, "\n{TAB}[{list_name}: ");
426
427            let style = valid.render();
428            let reset = valid.render_reset();
429            for (idx, val) in possible_values.iter().enumerate() {
430                if idx > 0 {
431                    styled.push_str(", ");
432                }
433                let _ = write!(styled, "{style}{}{reset}", Escape(val));
434            }
435
436            styled.push_str("]");
437        }
438    }
439}
440
441pub(crate) fn format_error_message(
442    message: &str,
443    styles: &Styles,
444    cmd: Option<&Command>,
445    usage: Option<&StyledStr>,
446) -> StyledStr {
447    let mut styled = StyledStr::new();
448    start_error(&mut styled, styles);
449    styled.push_str(message);
450    if let Some(usage) = usage {
451        put_usage(&mut styled, usage);
452    }
453    if let Some(cmd) = cmd {
454        try_help(&mut styled, styles, get_help_flag(cmd));
455    }
456    styled
457}
458
459/// Returns the singular or plural form on the verb to be based on the argument's value.
460fn singular_or_plural(n: usize) -> &'static str {
461    if n > 1 {
462        " were provided"
463    } else {
464        " was provided"
465    }
466}
467
468fn put_usage(styled: &mut StyledStr, usage: &StyledStr) {
469    styled.push_str("\n\n");
470    styled.push_styled(usage);
471}
472
473pub(crate) fn get_help_flag(cmd: &Command) -> Option<&'static str> {
474    if !cmd.is_disable_help_flag_set() {
475        Some("--help")
476    } else if cmd.has_subcommands() && !cmd.is_disable_help_subcommand_set() {
477        Some("help")
478    } else {
479        None
480    }
481}
482
483fn try_help(styled: &mut StyledStr, styles: &Styles, help: Option<&str>) {
484    if let Some(help) = help {
485        use std::fmt::Write as _;
486        let literal = &styles.get_literal();
487        let _ = write!(
488            styled,
489            "\n\nFor more information, try '{}{help}{}'.\n",
490            literal.render(),
491            literal.render_reset()
492        );
493    } else {
494        styled.push_str("\n");
495    }
496}
497
498#[cfg(feature = "error-context")]
499fn did_you_mean(styled: &mut StyledStr, styles: &Styles, context: &str, valid: &ContextValue) {
500    use std::fmt::Write as _;
501
502    let _ = write!(
503        styled,
504        "{TAB}{}tip:{}",
505        styles.get_valid().render(),
506        styles.get_valid().render_reset()
507    );
508    if let ContextValue::String(valid) = valid {
509        let _ = write!(
510            styled,
511            " a similar {context} exists: '{}{valid}{}'",
512            styles.get_valid().render(),
513            styles.get_valid().render_reset()
514        );
515    } else if let ContextValue::Strings(valid) = valid {
516        if valid.len() == 1 {
517            let _ = write!(styled, " a similar {context} exists: ",);
518        } else {
519            let _ = write!(styled, " some similar {context}s exist: ",);
520        }
521        for (i, valid) in valid.iter().enumerate() {
522            if i != 0 {
523                styled.push_str(", ");
524            }
525            let _ = write!(
526                styled,
527                "'{}{valid}{}'",
528                styles.get_valid().render(),
529                styles.get_valid().render_reset()
530            );
531        }
532    }
533}
534
535struct Escape<'s>(&'s str);
536
537impl<'s> std::fmt::Display for Escape<'s> {
538    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
539        if self.0.contains(char::is_whitespace) {
540            std::fmt::Debug::fmt(self.0, f)
541        } else {
542            self.0.fmt(f)
543        }
544    }
545}