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