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
20pub trait ErrorFormatter: Sized {
22 fn format_error(error: &crate::error::Error<Self>) -> StyledStr;
24}
25
26#[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#[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
415fn 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}