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
19pub trait ErrorFormatter: Sized {
21 fn format_error(error: &crate::error::Error<Self>) -> StyledStr;
23}
24
25#[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#[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
410fn 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}