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]
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#[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
414fn 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}