blob: d3ce6c64c19edcedc28422c64307928c32c778bf [file] [log] [blame]
#![allow(missing_copy_implementations)]
#![allow(missing_debug_implementations)]
#![cfg_attr(not(feature = "error-context"), allow(dead_code))]
#![cfg_attr(not(feature = "error-context"), allow(unused_imports))]
use crate::builder::Command;
use crate::builder::StyledStr;
#[cfg(feature = "error-context")]
use crate::error::ContextKind;
#[cfg(feature = "error-context")]
use crate::error::ContextValue;
use crate::error::ErrorKind;
use crate::output::TAB;
/// Defines how to format an error for displaying to the user
pub trait ErrorFormatter: Sized {
/// Stylize the error for the terminal
fn format_error(error: &crate::error::Error<Self>) -> StyledStr;
}
/// Report [`ErrorKind`]
///
/// No context is included.
///
/// **NOTE:** Consider removing the [`error-context`][crate::_features] default feature if using this to remove all
/// overhead for [`RichFormatter`].
#[non_exhaustive]
pub struct KindFormatter;
impl ErrorFormatter for KindFormatter {
fn format_error(error: &crate::error::Error<Self>) -> StyledStr {
let mut styled = StyledStr::new();
start_error(&mut styled);
if let Some(msg) = error.kind().as_str() {
styled.none(msg.to_owned());
} else if let Some(source) = error.inner.source.as_ref() {
styled.none(source.to_string());
} else {
styled.none("unknown cause");
}
styled.none("\n");
styled
}
}
/// Richly formatted error context
///
/// This follows the [rustc diagnostic style guide](https://rustc-dev-guide.rust-lang.org/diagnostics.html#suggestion-style-guide).
#[non_exhaustive]
#[cfg(feature = "error-context")]
pub struct RichFormatter;
#[cfg(feature = "error-context")]
impl ErrorFormatter for RichFormatter {
fn format_error(error: &crate::error::Error<Self>) -> StyledStr {
let mut styled = StyledStr::new();
start_error(&mut styled);
if !write_dynamic_context(error, &mut styled) {
if let Some(msg) = error.kind().as_str() {
styled.none(msg.to_owned());
} else if let Some(source) = error.inner.source.as_ref() {
styled.none(source.to_string());
} else {
styled.none("unknown cause");
}
}
let mut suggested = false;
if let Some(valid) = error.get(ContextKind::SuggestedSubcommand) {
styled.none("\n");
if !suggested {
styled.none("\n");
suggested = true;
}
did_you_mean(&mut styled, "subcommand", valid);
}
if let Some(valid) = error.get(ContextKind::SuggestedArg) {
styled.none("\n");
if !suggested {
styled.none("\n");
suggested = true;
}
did_you_mean(&mut styled, "argument", valid);
}
if let Some(valid) = error.get(ContextKind::SuggestedValue) {
styled.none("\n");
if !suggested {
styled.none("\n");
suggested = true;
}
did_you_mean(&mut styled, "value", valid);
}
let suggestions = error.get(ContextKind::Suggested);
if let Some(ContextValue::StyledStrs(suggestions)) = suggestions {
if !suggested {
styled.none("\n");
}
for suggestion in suggestions {
styled.none("\n");
styled.none(TAB);
styled.good("note: ");
styled.extend(suggestion.iter());
}
}
let usage = error.get(ContextKind::Usage);
if let Some(ContextValue::StyledStr(usage)) = usage {
put_usage(&mut styled, usage.clone());
}
try_help(&mut styled, error.inner.help_flag);
styled
}
}
fn start_error(styled: &mut StyledStr) {
styled.error("error:");
styled.none(" ");
}
#[must_use]
#[cfg(feature = "error-context")]
fn write_dynamic_context(error: &crate::error::Error, styled: &mut StyledStr) -> bool {
match error.kind() {
ErrorKind::ArgumentConflict => {
let invalid_arg = error.get(ContextKind::InvalidArg);
let prior_arg = error.get(ContextKind::PriorArg);
if let (Some(ContextValue::String(invalid_arg)), Some(prior_arg)) =
(invalid_arg, prior_arg)
{
if ContextValue::String(invalid_arg.clone()) == *prior_arg {
styled.none("the argument '");
styled.warning(invalid_arg);
styled.none("' cannot be used multiple times");
} else {
styled.none("the argument '");
styled.warning(invalid_arg);
styled.none("' cannot be used with");
match prior_arg {
ContextValue::Strings(values) => {
styled.none(":");
for v in values {
styled.none("\n");
styled.none(TAB);
styled.warning(&**v);
}
}
ContextValue::String(value) => {
styled.none(" '");
styled.warning(value);
styled.none("'");
}
_ => {
styled.none(" one or more of the other specified arguments");
}
}
}
true
} else {
false
}
}
ErrorKind::NoEquals => {
let invalid_arg = error.get(ContextKind::InvalidArg);
if let Some(ContextValue::String(invalid_arg)) = invalid_arg {
styled.none("equal sign is needed when assigning values to '");
styled.warning(invalid_arg);
styled.none("'");
true
} else {
false
}
}
ErrorKind::InvalidValue => {
let invalid_arg = error.get(ContextKind::InvalidArg);
let invalid_value = error.get(ContextKind::InvalidValue);
if let (
Some(ContextValue::String(invalid_arg)),
Some(ContextValue::String(invalid_value)),
) = (invalid_arg, invalid_value)
{
if invalid_value.is_empty() {
styled.none("a value is required for '");
styled.warning(invalid_arg);
styled.none("' but none was supplied");
} else {
styled.none("invalid value '");
styled.none(invalid_value);
styled.none("' for '");
styled.warning(invalid_arg);
styled.none("'");
}
let possible_values = error.get(ContextKind::ValidValue);
if let Some(ContextValue::Strings(possible_values)) = possible_values {
if !possible_values.is_empty() {
styled.none("\n");
styled.none(TAB);
styled.none("[possible values: ");
if let Some((last, elements)) = possible_values.split_last() {
for v in elements {
styled.good(escape(v));
styled.none(", ");
}
styled.good(escape(last));
}
styled.none("]");
}
}
true
} else {
false
}
}
ErrorKind::InvalidSubcommand => {
let invalid_sub = error.get(ContextKind::InvalidSubcommand);
if let Some(ContextValue::String(invalid_sub)) = invalid_sub {
styled.none("unrecognized subcommand '");
styled.warning(invalid_sub);
styled.none("'");
true
} else {
false
}
}
ErrorKind::MissingRequiredArgument => {
let invalid_arg = error.get(ContextKind::InvalidArg);
if let Some(ContextValue::Strings(invalid_arg)) = invalid_arg {
styled.none("the following required arguments were not provided:");
for v in invalid_arg {
styled.none("\n");
styled.none(TAB);
styled.good(&**v);
}
true
} else {
false
}
}
ErrorKind::MissingSubcommand => {
let invalid_sub = error.get(ContextKind::InvalidSubcommand);
if let Some(ContextValue::String(invalid_sub)) = invalid_sub {
styled.none("'");
styled.warning(invalid_sub);
styled.none("' requires a subcommand but one was not provided");
let possible_values = error.get(ContextKind::ValidSubcommand);
if let Some(ContextValue::Strings(possible_values)) = possible_values {
if !possible_values.is_empty() {
styled.none("\n");
styled.none(TAB);
styled.none("[subcommands: ");
if let Some((last, elements)) = possible_values.split_last() {
for v in elements {
styled.good(escape(v));
styled.none(", ");
}
styled.good(escape(last));
}
styled.none("]");
}
}
true
} else {
false
}
}
ErrorKind::InvalidUtf8 => false,
ErrorKind::TooManyValues => {
let invalid_arg = error.get(ContextKind::InvalidArg);
let invalid_value = error.get(ContextKind::InvalidValue);
if let (
Some(ContextValue::String(invalid_arg)),
Some(ContextValue::String(invalid_value)),
) = (invalid_arg, invalid_value)
{
styled.none("unexpected value '");
styled.warning(invalid_value);
styled.none("' for '");
styled.warning(invalid_arg);
styled.none("' found; no more were expected");
true
} else {
false
}
}
ErrorKind::TooFewValues => {
let invalid_arg = error.get(ContextKind::InvalidArg);
let actual_num_values = error.get(ContextKind::ActualNumValues);
let min_values = error.get(ContextKind::MinValues);
if let (
Some(ContextValue::String(invalid_arg)),
Some(ContextValue::Number(actual_num_values)),
Some(ContextValue::Number(min_values)),
) = (invalid_arg, actual_num_values, min_values)
{
let were_provided = singular_or_plural(*actual_num_values as usize);
styled.warning(min_values.to_string());
styled.none(" more values required by '");
styled.warning(invalid_arg);
styled.none("'; only ");
styled.warning(actual_num_values.to_string());
styled.none(were_provided);
true
} else {
false
}
}
ErrorKind::ValueValidation => {
let invalid_arg = error.get(ContextKind::InvalidArg);
let invalid_value = error.get(ContextKind::InvalidValue);
if let (
Some(ContextValue::String(invalid_arg)),
Some(ContextValue::String(invalid_value)),
) = (invalid_arg, invalid_value)
{
styled.none("invalid value '");
styled.warning(invalid_value);
styled.none("' for '");
styled.warning(invalid_arg);
if let Some(source) = error.inner.source.as_deref() {
styled.none("': ");
styled.none(source.to_string());
} else {
styled.none("'");
}
true
} else {
false
}
}
ErrorKind::WrongNumberOfValues => {
let invalid_arg = error.get(ContextKind::InvalidArg);
let actual_num_values = error.get(ContextKind::ActualNumValues);
let num_values = error.get(ContextKind::ExpectedNumValues);
if let (
Some(ContextValue::String(invalid_arg)),
Some(ContextValue::Number(actual_num_values)),
Some(ContextValue::Number(num_values)),
) = (invalid_arg, actual_num_values, num_values)
{
let were_provided = singular_or_plural(*actual_num_values as usize);
styled.warning(num_values.to_string());
styled.none(" values required for '");
styled.warning(invalid_arg);
styled.none("' but ");
styled.warning(actual_num_values.to_string());
styled.none(were_provided);
true
} else {
false
}
}
ErrorKind::UnknownArgument => {
let invalid_arg = error.get(ContextKind::InvalidArg);
if let Some(ContextValue::String(invalid_arg)) = invalid_arg {
styled.none("unexpected argument '");
styled.warning(invalid_arg.to_string());
styled.none("' found");
true
} else {
false
}
}
ErrorKind::DisplayHelp
| ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
| ErrorKind::DisplayVersion
| ErrorKind::Io
| ErrorKind::Format => false,
}
}
pub(crate) fn format_error_message(
message: &str,
cmd: Option<&Command>,
usage: Option<StyledStr>,
) -> StyledStr {
let mut styled = StyledStr::new();
start_error(&mut styled);
styled.none(message);
if let Some(usage) = usage {
put_usage(&mut styled, usage);
}
if let Some(cmd) = cmd {
try_help(&mut styled, get_help_flag(cmd));
}
styled
}
/// Returns the singular or plural form on the verb to be based on the argument's value.
fn singular_or_plural(n: usize) -> &'static str {
if n > 1 {
" were provided"
} else {
" was provided"
}
}
fn put_usage(styled: &mut StyledStr, usage: StyledStr) {
styled.none("\n\n");
styled.extend(usage.into_iter());
}
pub(crate) fn get_help_flag(cmd: &Command) -> Option<&'static str> {
if !cmd.is_disable_help_flag_set() {
Some("--help")
} else if cmd.has_subcommands() && !cmd.is_disable_help_subcommand_set() {
Some("help")
} else {
None
}
}
fn try_help(styled: &mut StyledStr, help: Option<&str>) {
if let Some(help) = help {
styled.none("\n\nFor more information, try '");
styled.literal(help.to_owned());
styled.none("'.\n");
} else {
styled.none("\n");
}
}
#[cfg(feature = "error-context")]
fn did_you_mean(styled: &mut StyledStr, context: &str, valid: &ContextValue) {
if let ContextValue::String(valid) = valid {
styled.none(TAB);
styled.good("note: ");
styled.none(context);
styled.none(" '");
styled.good(valid);
styled.none("' exists");
} else if let ContextValue::Strings(valid) = valid {
styled.none(TAB);
styled.good("note: ");
styled.none(context);
if valid.len() > 1 {
styled.none("s");
}
styled.none(" ");
for (i, valid) in valid.iter().enumerate() {
if i != 0 {
styled.none(", ");
}
styled.none("'");
styled.good(valid);
styled.none("'");
}
if valid.len() == 1 {
styled.none(" exists");
} else {
styled.none(" exist");
}
}
}
fn escape(s: impl AsRef<str>) -> String {
let s = s.as_ref();
if s.contains(char::is_whitespace) {
format!("{s:?}")
} else {
s.to_owned()
}
}