Repository: clap-rs/clap
Pull request #4111 (2022-08-24 17:29:58)

perf(error): Allow custmizing formatting
For now, there isn't much a custom implementation can do.

Going from `Rich` to `Null` drops about 6 KiB from the binary

This is a part of #1365 and #1384


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9421322e..42b54103 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -60,6 +60,7 @@ MSRV is now 1.60.0
 
 - `Arg::num_args` now accepts ranges, allowing setting both the minimum and maximum number of values per occurrence
 - Added `TypedValueParser::map` to make it easier to reuse existing value parsers
+- *(error)* `Error::apply` for changing the formatter for dropping binary size
 - *(help)* Show `PossibleValue::help` in long help (`--help`) (#3312)
 
 ### Fixes
diff --git a/src/builder/command.rs b/src/builder/command.rs
index 8f50bd74..4d81e93f 100644
--- a/src/builder/command.rs
+++ b/src/builder/command.rs
@@ -4390,12 +4390,11 @@ impl Command {
         self.disp_ord.unwrap_or(999)
     }
 
-    pub(crate) fn write_help_err(&self, mut use_long: bool, stream: Stream) -> Colorizer {
+    pub(crate) fn write_help_err(&self, mut use_long: bool) -> StyledStr {
         debug!(
-            "Command::write_help_err: {}, use_long={:?}, stream={:?}",
+            "Command::write_help_err: {}, use_long={:?}",
             self.get_display_name().unwrap_or_else(|| self.get_name()),
             use_long && self.use_long_help(),
-            stream
         );
 
         use_long = use_long && self.use_long_help();
@@ -4404,14 +4403,14 @@ impl Command {
         let mut styled = StyledStr::new();
         Help::new(&mut styled, self, &usage, use_long).write_help();
 
-        Colorizer::new(stream, self.color_help()).with_content(styled)
+        styled
     }
 
-    pub(crate) fn write_version_err(&self, use_long: bool) -> Colorizer {
+    pub(crate) fn write_version_err(&self, use_long: bool) -> StyledStr {
         let msg = self._render_version(use_long);
         let mut styled = StyledStr::new();
         styled.none(msg);
-        Colorizer::new(Stream::Stdout, self.color_help()).with_content(styled)
+        styled
     }
 
     pub(crate) fn use_long_help(&self) -> bool {
diff --git a/src/builder/mod.rs b/src/builder/mod.rs
index b717d598..a450cbee 100644
--- a/src/builder/mod.rs
+++ b/src/builder/mod.rs
@@ -33,6 +33,7 @@ pub use possible_value::PossibleValue;
 pub use range::ValueRange;
 pub use resettable::IntoResettable;
 pub use resettable::Resettable;
+pub use styled_str::StyledStr;
 pub use value_hint::ValueHint;
 pub use value_parser::_AutoValueParser;
 pub use value_parser::via_prelude;
@@ -55,7 +56,6 @@ pub use value_parser::_AnonymousValueParser;
 
 #[allow(unused_imports)]
 pub(crate) use self::str::Inner as StrInner;
-pub(crate) use self::styled_str::StyledStr;
 pub(crate) use action::CountType;
 pub(crate) use arg::render_arg_val;
 pub(crate) use arg_settings::{ArgFlags, ArgSettings};
diff --git a/src/builder/styled_str.rs b/src/builder/styled_str.rs
index ad2a697b..afe2545a 100644
--- a/src/builder/styled_str.rs
+++ b/src/builder/styled_str.rs
@@ -1,10 +1,12 @@
+/// Terminal-styling container
 #[derive(Clone, Default, Debug, PartialEq, Eq)]
-pub(crate) struct StyledStr {
+pub struct StyledStr {
     pieces: Vec<(Option<Style>, String)>,
 }
 
 impl StyledStr {
-    pub(crate) const fn new() -> Self {
+    /// Create an empty buffer
+    pub const fn new() -> Self {
         Self { pieces: Vec::new() }
     }
 
diff --git a/src/error/context.rs b/src/error/context.rs
index 4b2223c1..59748259 100644
--- a/src/error/context.rs
+++ b/src/error/context.rs
@@ -1,5 +1,5 @@
 /// Semantics for a piece of error information
-#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
 #[non_exhaustive]
 pub enum ContextKind {
     /// The cause of the error
@@ -34,6 +34,35 @@ pub enum ContextKind {
     Custom,
 }
 
+impl ContextKind {
+    /// End-user description of the error case, where relevant
+    pub fn as_str(self) -> Option<&'static str> {
+        match self {
+            Self::InvalidSubcommand => Some("Invalid Subcommand"),
+            Self::InvalidArg => Some("Invalid Argument"),
+            Self::PriorArg => Some("Prior Argument"),
+            Self::ValidValue => Some("Value Value"),
+            Self::InvalidValue => Some("Invalid Value"),
+            Self::ActualNumValues => Some("Actual Number of Values"),
+            Self::ExpectedNumValues => Some("Expected Number of Values"),
+            Self::MinValues => Some("Minimum Number of Values"),
+            Self::SuggestedCommand => Some("Suggested Command"),
+            Self::SuggestedSubcommand => Some("Suggested Subcommand"),
+            Self::SuggestedArg => Some("Suggested Argument"),
+            Self::SuggestedValue => Some("Suggested Value"),
+            Self::TrailingArg => Some("Trailing Argument"),
+            Self::Usage => None,
+            Self::Custom => None,
+        }
+    }
+}
+
+impl std::fmt::Display for ContextKind {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.as_str().unwrap_or_default().fmt(f)
+    }
+}
+
 /// A piece of error information
 #[derive(Clone, Debug, PartialEq, Eq)]
 #[non_exhaustive]
@@ -49,3 +78,15 @@ pub enum ContextValue {
     /// A single value
     Number(isize),
 }
+
+impl std::fmt::Display for ContextValue {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::None => "".fmt(f),
+            Self::Bool(v) => v.fmt(f),
+            Self::String(v) => v.fmt(f),
+            Self::Strings(v) => v.join(", ").fmt(f),
+            Self::Number(v) => v.fmt(f),
+        }
+    }
+}
diff --git a/src/error/format.rs b/src/error/format.rs
new file mode 100644
index 00000000..002d4428
--- /dev/null
+++ b/src/error/format.rs
@@ -0,0 +1,448 @@
+#![allow(missing_copy_implementations)]
+#![allow(missing_debug_implementations)]
+
+use crate::builder::Command;
+use crate::builder::StyledStr;
+use crate::error::ContextKind;
+use crate::error::ContextValue;
+use crate::error::ErrorKind;
+
+/// 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<Self>) -> StyledStr;
+}
+
+/// No error information reported
+#[non_exhaustive]
+pub struct NullFormatter;
+
+impl ErrorFormatter for NullFormatter {
+    fn format_error(_error: &crate::Error<Self>) -> StyledStr {
+        StyledStr::new()
+    }
+}
+
+/// Dump the error information reported
+#[non_exhaustive]
+pub struct RawFormatter;
+
+impl ErrorFormatter for RawFormatter {
+    fn format_error(error: &crate::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");
+
+        if error.context().next().is_some() {
+            styled.none("\n");
+        }
+        for (kind, value) in error.context() {
+            if let Some(kind) = kind.as_str() {
+                styled.none(kind);
+                styled.none(": ");
+                styled.none(value.to_string());
+            } else {
+                styled.none(value.to_string());
+            }
+            styled.none("\n");
+        }
+
+        styled
+    }
+}
+
+/// Default, rich error format
+#[non_exhaustive]
+pub struct RichFormatter;
+
+impl ErrorFormatter for RichFormatter {
+    fn format_error(error: &crate::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 usage = error.get(ContextKind::Usage);
+        if let Some(ContextValue::String(usage)) = usage {
+            put_usage(&mut styled, usage);
+        }
+
+        try_help(&mut styled, error.inner.help_flag);
+
+        styled
+    }
+}
+
+fn start_error(styled: &mut StyledStr) {
+    styled.error("error:");
+    styled.none(" ");
+}
+
+#[must_use]
+fn write_dynamic_context(error: &crate::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)
+            {
+                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.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("The argument '");
+                    styled.warning(invalid_arg);
+                    styled.none("' requires a value but none was supplied");
+                } else {
+                    styled.none(quote(invalid_value));
+                    styled.none(" isn't a valid value 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\t[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("]");
+                    }
+                }
+
+                let suggestion = error.get(ContextKind::SuggestedValue);
+                if let Some(ContextValue::String(suggestion)) = suggestion {
+                    styled.none("\n\n\tDid you mean ");
+                    styled.good(quote(suggestion));
+                    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("The subcommand '");
+                styled.warning(invalid_sub);
+                styled.none("' wasn't recognized");
+
+                let valid_sub = error.get(ContextKind::SuggestedSubcommand);
+                if let Some(ContextValue::String(valid_sub)) = valid_sub {
+                    styled.none("\n\n\tDid you mean ");
+                    styled.good(valid_sub);
+                    styled.none("?");
+                }
+
+                let suggestion = error.get(ContextKind::SuggestedCommand);
+                if let Some(ContextValue::String(suggestion)) = suggestion {
+                    styled.none(
+            "\n\nIf you believe you received this message in error, try re-running with '",
+        );
+                    styled.good(suggestion);
+                    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.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");
+                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("The value '");
+                styled.warning(invalid_value);
+                styled.none("' was provided to '");
+                styled.warning(invalid_arg);
+                styled.none("' but it wasn't expecting any more values");
+                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.none("The argument '");
+                styled.warning(invalid_arg);
+                styled.none("' requires at least ");
+                styled.warning(min_values.to_string());
+                styled.none(" values but 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(quote(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.none("The argument '");
+                styled.warning(invalid_arg);
+                styled.none("' requires ");
+                styled.warning(num_values.to_string());
+                styled.none(" values, 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("Found argument '");
+                styled.warning(invalid_arg.to_string());
+                styled.none("' which wasn't expected, or isn't valid in this context");
+
+                let valid_sub = error.get(ContextKind::SuggestedSubcommand);
+                let valid_arg = error.get(ContextKind::SuggestedArg);
+                match (valid_sub, valid_arg) {
+                    (
+                        Some(ContextValue::String(valid_sub)),
+                        Some(ContextValue::String(valid_arg)),
+                    ) => {
+                        styled.none("\n\n\tDid you mean ");
+                        styled.none("to put '");
+                        styled.good(valid_arg);
+                        styled.none("' after the subcommand '");
+                        styled.good(valid_sub);
+                        styled.none("'?");
+                    }
+                    (None, Some(ContextValue::String(valid_arg))) => {
+                        styled.none("\n\n\tDid you mean '");
+                        styled.good(valid_arg);
+                        styled.none("'?");
+                    }
+                    (_, _) => {}
+                }
+
+                let invalid_arg = error.get(ContextKind::InvalidArg);
+                if let Some(ContextValue::String(invalid_arg)) = invalid_arg {
+                    if invalid_arg.starts_with('-') {
+                        styled.none(format!(
+                                "\n\n\tIf you tried to supply `{}` as a value rather than a flag, use `-- {}`",
+                                invalid_arg, invalid_arg
+                            ));
+                    }
+
+                    let trailing_arg = error.get(ContextKind::TrailingArg);
+                    if trailing_arg == Some(&ContextValue::Bool(true)) {
+                        styled.none(format!(
+                            "\n\n\tIf you tried to supply `{}` as a subcommand, remove the '--' before it.",
+                            invalid_arg
+                        ));
+                    }
+                }
+                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<String>,
+) -> 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: impl Into<String>) {
+    styled.none("\n\n");
+    styled.none(usage);
+}
+
+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.good(help.to_owned());
+        styled.none("\n");
+    } else {
+        styled.none("\n");
+    }
+}
+
+fn quote(s: impl AsRef<str>) -> String {
+    let s = s.as_ref();
+    format!("{:?}", s)
+}
+
+fn escape(s: impl AsRef<str>) -> String {
+    let s = s.as_ref();
+    if s.contains(char::is_whitespace) {
+        quote(s)
+    } else {
+        s.to_owned()
+    }
+}
diff --git a/src/error/kind.rs b/src/error/kind.rs
index 754d0df3..77bbad44 100644
--- a/src/error/kind.rs
+++ b/src/error/kind.rs
@@ -1,5 +1,5 @@
 /// Command line argument parser kind of error
-#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
 #[non_exhaustive]
 pub enum ErrorKind {
     /// Occurs when an [`Arg`][crate::Arg] has a set of possible values,
diff --git a/src/error/mod.rs b/src/error/mod.rs
index e73e047b..b423f8da 100644
--- a/src/error/mod.rs
+++ b/src/error/mod.rs
@@ -15,14 +15,20 @@ use crate::builder::StyledStr;
 use crate::output::fmt::Colorizer;
 use crate::output::fmt::Stream;
 use crate::parser::features::suggestions;
+use crate::util::FlatMap;
 use crate::util::{color::ColorChoice, safe_exit, SUCCESS_CODE, USAGE_CODE};
 use crate::Command;
 
 mod context;
+mod format;
 mod kind;
 
 pub use context::ContextKind;
 pub use context::ContextValue;
+pub use format::ErrorFormatter;
+pub use format::NullFormatter;
+pub use format::RawFormatter;
+pub use format::RichFormatter;
 pub use kind::ErrorKind;
 
 /// Short hand for [`Result`] type
@@ -35,23 +41,24 @@ pub type Result<T, E = Error> = StdResult<T, E>;
 /// See [`Command::error`] to create an error.
 ///
 /// [`Command::error`]: crate::Command::error
-#[derive(Debug)]
-pub struct Error {
+pub struct Error<F: ErrorFormatter = RichFormatter> {
     inner: Box<ErrorInner>,
+    phantom: std::marker::PhantomData<F>,
 }
 
 #[derive(Debug)]
 struct ErrorInner {
     kind: ErrorKind,
-    context: Vec<(ContextKind, ContextValue)>,
+    context: FlatMap<ContextKind, ContextValue>,
     message: Option<Message>,
     source: Option<Box<dyn error::Error + Send + Sync>>,
     help_flag: Option<&'static str>,
     color_when: ColorChoice,
+    color_help_when: ColorChoice,
     backtrace: Option<Backtrace>,
 }
 
-impl Error {
+impl<F: ErrorFormatter> Error<F> {
     /// Create an unformatted error
     ///
     /// This is for you need to pass the error up to
@@ -75,6 +82,28 @@ impl Error {
         self.with_cmd(cmd)
     }
 
+    /// Apply an alternative formatter to the error
+    ///
+    /// # Example
+    ///
+    /// ```rust
+    /// # use clap::Command;
+    /// # use clap::Arg;
+    /// # use clap::error::NullFormatter;
+    /// let cmd = Command::new("foo")
+    ///     .arg(Arg::new("input").required(true));
+    /// let matches = cmd
+    ///     .try_get_matches_from(["foo", "input.txt"])
+    ///     .map_err(|e| e.apply::<NullFormatter>())
+    ///     .unwrap_or_else(|e| e.exit());
+    /// ```
+    pub fn apply<EF: ErrorFormatter>(self) -> Error<EF> {
+        Error {
+            inner: self.inner,
+            phantom: Default::default(),
+        }
+    }
+
     /// Type of error for programmatic processing
     pub fn kind(&self) -> ErrorKind {
         self.inner.kind
@@ -85,6 +114,12 @@ impl Error {
         self.inner.context.iter().map(|(k, v)| (*k, v))
     }
 
+    /// Lookup a piece of context
+    #[inline(never)]
+    pub fn get(&self, kind: ContextKind) -> Option<&ContextValue> {
+        self.inner.context.get(&kind)
+    }
+
     /// Should the message be written to `stdout` or not?
     #[inline]
     pub fn use_stderr(&self) -> bool {
@@ -132,31 +167,41 @@ impl Error {
     /// };
     /// ```
     pub fn print(&self) -> io::Result<()> {
-        self.formatted().print()
+        let style = self.formatted();
+        let color_when = if self.kind() == ErrorKind::DisplayHelp {
+            self.inner.color_help_when
+        } else {
+            self.inner.color_when
+        };
+        let c = Colorizer::new(self.stream(), color_when).with_content(style.into_owned());
+        c.print()
     }
 
     fn new(kind: ErrorKind) -> Self {
         Self {
             inner: Box::new(ErrorInner {
                 kind,
-                context: Vec::new(),
+                context: FlatMap::new(),
                 message: None,
                 source: None,
                 help_flag: None,
                 color_when: ColorChoice::Never,
+                color_help_when: ColorChoice::Never,
                 backtrace: Backtrace::new(),
             }),
+            phantom: Default::default(),
         }
     }
 
     #[inline(never)]
-    fn for_app(kind: ErrorKind, cmd: &Command, colorizer: Colorizer) -> Self {
-        Self::new(kind).set_message(colorizer).with_cmd(cmd)
+    fn for_app(kind: ErrorKind, cmd: &Command, styled: StyledStr) -> Self {
+        Self::new(kind).set_message(styled).with_cmd(cmd)
     }
 
     pub(crate) fn with_cmd(self, cmd: &Command) -> Self {
         self.set_color(cmd.get_color())
-            .set_help_flag(get_help_flag(cmd))
+            .set_colored_help(cmd.color_help())
+            .set_help_flag(format::get_help_flag(cmd))
     }
 
     pub(crate) fn set_message(mut self, message: impl Into<Message>) -> Self {
@@ -174,6 +219,11 @@ impl Error {
         self
     }
 
+    pub(crate) fn set_colored_help(mut self, color_help_when: ColorChoice) -> Self {
+        self.inner.color_help_when = color_help_when;
+        self
+    }
+
     pub(crate) fn set_help_flag(mut self, help_flag: Option<&'static str>) -> Self {
         self.inner.help_flag = help_flag;
         self
@@ -186,7 +236,7 @@ impl Error {
         kind: ContextKind,
         value: ContextValue,
     ) -> Self {
-        self.inner.context.push((kind, value));
+        self.inner.context.insert_unchecked(kind, value);
         self
     }
 
@@ -196,32 +246,24 @@ impl Error {
         mut self,
         context: [(ContextKind, ContextValue); N],
     ) -> Self {
-        self.inner.context.extend(context);
+        self.inner.context.extend_unchecked(context);
         self
     }
 
-    #[inline(never)]
-    fn get_context(&self, kind: ContextKind) -> Option<&ContextValue> {
-        self.inner
-            .context
-            .iter()
-            .find_map(|(k, v)| (*k == kind).then(|| v))
-    }
-
-    pub(crate) fn display_help(cmd: &Command, colorizer: Colorizer) -> Self {
-        Self::for_app(ErrorKind::DisplayHelp, cmd, colorizer)
+    pub(crate) fn display_help(cmd: &Command, styled: StyledStr) -> Self {
+        Self::for_app(ErrorKind::DisplayHelp, cmd, styled)
     }
 
-    pub(crate) fn display_help_error(cmd: &Command, colorizer: Colorizer) -> Self {
+    pub(crate) fn display_help_error(cmd: &Command, styled: StyledStr) -> Self {
         Self::for_app(
             ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand,
             cmd,
-            colorizer,
+            styled,
         )
     }
 
-    pub(crate) fn display_version(cmd: &Command, colorizer: Colorizer) -> Self {
-        Self::for_app(ErrorKind::DisplayVersion, cmd, colorizer)
+    pub(crate) fn display_version(cmd: &Command, styled: StyledStr) -> Self {
+        Self::for_app(ErrorKind::DisplayVersion, cmd, styled)
     }
 
     pub(crate) fn argument_conflict(
@@ -450,354 +492,42 @@ impl Error {
             ])
     }
 
-    fn formatted(&self) -> Cow<'_, Colorizer> {
+    fn formatted(&self) -> Cow<'_, StyledStr> {
         if let Some(message) = self.inner.message.as_ref() {
             message.formatted()
         } else {
-            let mut styled = StyledStr::new();
-            start_error(&mut styled);
-
-            if !self.write_dynamic_context(&mut styled) {
-                if let Some(msg) = self.kind().as_str() {
-                    styled.none(msg.to_owned());
-                } else if let Some(source) = self.inner.source.as_ref() {
-                    styled.none(source.to_string());
-                } else {
-                    styled.none("Unknown cause");
-                }
-            }
-
-            let usage = self.get_context(ContextKind::Usage);
-            if let Some(ContextValue::String(usage)) = usage {
-                put_usage(&mut styled, usage);
-            }
-
-            try_help(&mut styled, self.inner.help_flag);
-
-            let c = Colorizer::new(self.stream(), self.inner.color_when).with_content(styled);
-            Cow::Owned(c)
-        }
-    }
-
-    #[must_use]
-    fn write_dynamic_context(&self, styled: &mut StyledStr) -> bool {
-        match self.kind() {
-            ErrorKind::ArgumentConflict => {
-                let invalid_arg = self.get_context(ContextKind::InvalidArg);
-                let prior_arg = self.get_context(ContextKind::PriorArg);
-                if let (Some(ContextValue::String(invalid_arg)), Some(prior_arg)) =
-                    (invalid_arg, prior_arg)
-                {
-                    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.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 = self.get_context(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 = self.get_context(ContextKind::InvalidArg);
-                let invalid_value = self.get_context(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("The argument '");
-                        styled.warning(invalid_arg);
-                        styled.none("' requires a value but none was supplied");
-                    } else {
-                        styled.none(quote(invalid_value));
-                        styled.none(" isn't a valid value for '");
-                        styled.warning(invalid_arg);
-                        styled.none("'");
-                    }
-
-                    let possible_values = self.get_context(ContextKind::ValidValue);
-                    if let Some(ContextValue::Strings(possible_values)) = possible_values {
-                        if !possible_values.is_empty() {
-                            styled.none("\n\t[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("]");
-                        }
-                    }
-
-                    let suggestion = self.get_context(ContextKind::SuggestedValue);
-                    if let Some(ContextValue::String(suggestion)) = suggestion {
-                        styled.none("\n\n\tDid you mean ");
-                        styled.good(quote(suggestion));
-                        styled.none("?");
-                    }
-                    true
-                } else {
-                    false
-                }
-            }
-            ErrorKind::InvalidSubcommand => {
-                let invalid_sub = self.get_context(ContextKind::InvalidSubcommand);
-                if let Some(ContextValue::String(invalid_sub)) = invalid_sub {
-                    styled.none("The subcommand '");
-                    styled.warning(invalid_sub);
-                    styled.none("' wasn't recognized");
-
-                    let valid_sub = self.get_context(ContextKind::SuggestedSubcommand);
-                    if let Some(ContextValue::String(valid_sub)) = valid_sub {
-                        styled.none("\n\n\tDid you mean ");
-                        styled.good(valid_sub);
-                        styled.none("?");
-                    }
-
-                    let suggestion = self.get_context(ContextKind::SuggestedCommand);
-                    if let Some(ContextValue::String(suggestion)) = suggestion {
-                        styled.none(
-            "\n\nIf you believe you received this message in error, try re-running with '",
-        );
-                        styled.good(suggestion);
-                        styled.none("'");
-                    }
-                    true
-                } else {
-                    false
-                }
-            }
-            ErrorKind::MissingRequiredArgument => {
-                let invalid_arg = self.get_context(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.good(&**v);
-                    }
-                    true
-                } else {
-                    false
-                }
-            }
-            ErrorKind::MissingSubcommand => {
-                let invalid_sub = self.get_context(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");
-                    true
-                } else {
-                    false
-                }
-            }
-            ErrorKind::InvalidUtf8 => false,
-            ErrorKind::TooManyValues => {
-                let invalid_arg = self.get_context(ContextKind::InvalidArg);
-                let invalid_value = self.get_context(ContextKind::InvalidValue);
-                if let (
-                    Some(ContextValue::String(invalid_arg)),
-                    Some(ContextValue::String(invalid_value)),
-                ) = (invalid_arg, invalid_value)
-                {
-                    styled.none("The value '");
-                    styled.warning(invalid_value);
-                    styled.none("' was provided to '");
-                    styled.warning(invalid_arg);
-                    styled.none("' but it wasn't expecting any more values");
-                    true
-                } else {
-                    false
-                }
-            }
-            ErrorKind::TooFewValues => {
-                let invalid_arg = self.get_context(ContextKind::InvalidArg);
-                let actual_num_values = self.get_context(ContextKind::ActualNumValues);
-                let min_values = self.get_context(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 = Error::singular_or_plural(*actual_num_values as usize);
-                    styled.none("The argument '");
-                    styled.warning(invalid_arg);
-                    styled.none("' requires at least ");
-                    styled.warning(min_values.to_string());
-                    styled.none(" values but only ");
-                    styled.warning(actual_num_values.to_string());
-                    styled.none(were_provided);
-                    true
-                } else {
-                    false
-                }
-            }
-            ErrorKind::ValueValidation => {
-                let invalid_arg = self.get_context(ContextKind::InvalidArg);
-                let invalid_value = self.get_context(ContextKind::InvalidValue);
-                if let (
-                    Some(ContextValue::String(invalid_arg)),
-                    Some(ContextValue::String(invalid_value)),
-                ) = (invalid_arg, invalid_value)
-                {
-                    styled.none("Invalid value ");
-                    styled.warning(quote(invalid_value));
-                    styled.none(" for '");
-                    styled.warning(invalid_arg);
-                    if let Some(source) = self.inner.source.as_deref() {
-                        styled.none("': ");
-                        styled.none(source.to_string());
-                    } else {
-                        styled.none("'");
-                    }
-                    true
-                } else {
-                    false
-                }
-            }
-            ErrorKind::WrongNumberOfValues => {
-                let invalid_arg = self.get_context(ContextKind::InvalidArg);
-                let actual_num_values = self.get_context(ContextKind::ActualNumValues);
-                let num_values = self.get_context(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 = Error::singular_or_plural(*actual_num_values as usize);
-                    styled.none("The argument '");
-                    styled.warning(invalid_arg);
-                    styled.none("' requires ");
-                    styled.warning(num_values.to_string());
-                    styled.none(" values, but ");
-                    styled.warning(actual_num_values.to_string());
-                    styled.none(were_provided);
-                    true
-                } else {
-                    false
-                }
-            }
-            ErrorKind::UnknownArgument => {
-                let invalid_arg = self.get_context(ContextKind::InvalidArg);
-                if let Some(ContextValue::String(invalid_arg)) = invalid_arg {
-                    styled.none("Found argument '");
-                    styled.warning(invalid_arg.to_string());
-                    styled.none("' which wasn't expected, or isn't valid in this context");
-
-                    let valid_sub = self.get_context(ContextKind::SuggestedSubcommand);
-                    let valid_arg = self.get_context(ContextKind::SuggestedArg);
-                    match (valid_sub, valid_arg) {
-                        (
-                            Some(ContextValue::String(valid_sub)),
-                            Some(ContextValue::String(valid_arg)),
-                        ) => {
-                            styled.none("\n\n\tDid you mean ");
-                            styled.none("to put '");
-                            styled.good(valid_arg);
-                            styled.none("' after the subcommand '");
-                            styled.good(valid_sub);
-                            styled.none("'?");
-                        }
-                        (None, Some(ContextValue::String(valid_arg))) => {
-                            styled.none("\n\n\tDid you mean '");
-                            styled.good(valid_arg);
-                            styled.none("'?");
-                        }
-                        (_, _) => {}
-                    }
-
-                    let invalid_arg = self.get_context(ContextKind::InvalidArg);
-                    if let Some(ContextValue::String(invalid_arg)) = invalid_arg {
-                        if invalid_arg.starts_with('-') {
-                            styled.none(format!(
-                                "\n\n\tIf you tried to supply `{}` as a value rather than a flag, use `-- {}`",
-                                invalid_arg, invalid_arg
-                            ));
-                        }
-
-                        let trailing_arg = self.get_context(ContextKind::TrailingArg);
-                        if trailing_arg == Some(&ContextValue::Bool(true)) {
-                            styled.none(format!(
-                            "\n\n\tIf you tried to supply `{}` as a subcommand, remove the '--' before it.",
-                            invalid_arg
-                        ));
-                        }
-                    }
-                    true
-                } else {
-                    false
-                }
-            }
-            ErrorKind::DisplayHelp
-            | ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
-            | ErrorKind::DisplayVersion
-            | ErrorKind::Io
-            | ErrorKind::Format => false,
-        }
-    }
-
-    /// 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"
+            let styled = F::format_error(self);
+            Cow::Owned(styled)
         }
     }
 }
 
-impl From<io::Error> for Error {
+impl<F: ErrorFormatter> From<io::Error> for Error<F> {
     fn from(e: io::Error) -> Self {
         Error::raw(ErrorKind::Io, e)
     }
 }
 
-impl From<fmt::Error> for Error {
+impl<F: ErrorFormatter> From<fmt::Error> for Error<F> {
     fn from(e: fmt::Error) -> Self {
         Error::raw(ErrorKind::Format, e)
     }
 }
 
-impl error::Error for Error {
+impl<F: ErrorFormatter> std::fmt::Debug for Error<F> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
+        self.inner.fmt(f)
+    }
+}
+
+impl<F: ErrorFormatter> error::Error for Error<F> {
     #[allow(trivial_casts)]
     fn source(&self) -> Option<&(dyn error::Error + 'static)> {
         self.inner.source.as_ref().map(|e| e.as_ref() as _)
     }
 }
 
-impl Display for Error {
+impl<F: ErrorFormatter> Display for Error<F> {
     fn fmt(&self, f: &mut Formatter) -> fmt::Result {
         // Assuming `self.message` already has a trailing newline, from `try_help` or similar
         write!(f, "{}", self.formatted())?;
@@ -810,54 +540,10 @@ impl Display for Error {
     }
 }
 
-fn start_error(styled: &mut StyledStr) {
-    styled.error("error:");
-    styled.none(" ");
-}
-
-fn put_usage(styled: &mut StyledStr, usage: impl Into<String>) {
-    styled.none("\n\n");
-    styled.none(usage);
-}
-
-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.good(help.to_owned());
-        styled.none("\n");
-    } else {
-        styled.none("\n");
-    }
-}
-
-fn quote(s: impl AsRef<str>) -> String {
-    let s = s.as_ref();
-    format!("{:?}", s)
-}
-
-fn escape(s: impl AsRef<str>) -> String {
-    let s = s.as_ref();
-    if s.contains(char::is_whitespace) {
-        quote(s)
-    } else {
-        s.to_owned()
-    }
-}
-
 #[derive(Clone, Debug)]
 pub(crate) enum Message {
     Raw(String),
-    Formatted(Colorizer),
+    Formatted(StyledStr),
 }
 
 impl Message {
@@ -867,30 +553,22 @@ impl Message {
                 let mut message = String::new();
                 std::mem::swap(s, &mut message);
 
-                let mut styled = StyledStr::new();
-                start_error(&mut styled);
-                styled.none(message);
-                put_usage(&mut styled, usage);
-                try_help(&mut styled, get_help_flag(cmd));
+                let styled = format::format_error_message(&message, Some(cmd), Some(usage));
 
-                let c = Colorizer::new(Stream::Stderr, cmd.get_color()).with_content(styled);
-                *self = Self::Formatted(c);
+                *self = Self::Formatted(styled);
             }
             Message::Formatted(_) => {}
         }
     }
 
-    fn formatted(&self) -> Cow<Colorizer> {
+    fn formatted(&self) -> Cow<StyledStr> {
         match self {
             Message::Raw(s) => {
-                let mut styled = StyledStr::new();
-                start_error(&mut styled);
-                styled.none(s);
+                let styled = format::format_error_message(s, None, None);
 
-                let c = Colorizer::new(Stream::Stderr, ColorChoice::Never).with_content(styled);
-                Cow::Owned(c)
+                Cow::Owned(styled)
             }
-            Message::Formatted(c) => Cow::Borrowed(c),
+            Message::Formatted(s) => Cow::Borrowed(s),
         }
     }
 }
@@ -901,8 +579,8 @@ impl From<String> for Message {
     }
 }
 
-impl From<Colorizer> for Message {
-    fn from(inner: Colorizer) -> Self {
+impl From<StyledStr> for Message {
+    fn from(inner: StyledStr) -> Self {
         Self::Formatted(inner)
     }
 }
diff --git a/src/parser/parser.rs b/src/parser/parser.rs
index f5ed3ba2..99f91bad 100644
--- a/src/parser/parser.rs
+++ b/src/parser/parser.rs
@@ -14,7 +14,6 @@ use crate::builder::{Arg, Command};
 use crate::error::Error as ClapError;
 use crate::error::Result as ClapResult;
 use crate::mkeymap::KeyType;
-use crate::output::fmt::Stream;
 use crate::output::Usage;
 use crate::parser::features::suggestions;
 use crate::parser::AnyValue;
@@ -622,7 +621,7 @@ impl<'cmd> Parser<'cmd> {
         };
         let parser = Parser::new(sc);
 
-        Err(parser.help_err(true, Stream::Stdout))
+        Err(parser.help_err(true))
     }
 
     fn is_new_arg(&self, next: &clap_lex::ParsedArg<'_>, current_positional: &Arg) -> bool {
@@ -1236,7 +1235,7 @@ impl<'cmd> Parser<'cmd> {
                     None => true,
                 };
                 debug!("Help: use_long={}", use_long);
-                Err(self.help_err(use_long, Stream::Stdout))
+                Err(self.help_err(use_long))
             }
             ArgAction::Version => {
                 debug_assert_eq!(raw_vals, Vec::<OsString>::new());
@@ -1529,14 +1528,14 @@ impl<'cmd> Parser<'cmd> {
         )
     }
 
-    fn help_err(&self, use_long: bool, stream: Stream) -> ClapError {
-        let c = self.cmd.write_help_err(use_long, stream);
-        ClapError::display_help(self.cmd, c)
+    fn help_err(&self, use_long: bool) -> ClapError {
+        let styled = self.cmd.write_help_err(use_long);
+        ClapError::display_help(self.cmd, styled)
     }
 
     fn version_err(&self, use_long: bool) -> ClapError {
-        let c = self.cmd.write_version_err(use_long);
-        ClapError::display_version(self.cmd, c)
+        let styled = self.cmd.write_version_err(use_long);
+        ClapError::display_version(self.cmd, styled)
     }
 }
 
diff --git a/src/parser/validator.rs b/src/parser/validator.rs
index e945abf5..118165a1 100644
--- a/src/parser/validator.rs
+++ b/src/parser/validator.rs
@@ -1,7 +1,6 @@
 // Internal
 use crate::builder::{Arg, ArgPredicate, Command, PossibleValue};
 use crate::error::{Error, Result as ClapResult};
-use crate::output::fmt::Stream;
 use crate::output::Usage;
 use crate::parser::{ArgMatcher, ParseState};
 use crate::util::ChildGraph;
@@ -58,7 +57,7 @@ impl<'cmd> Validator<'cmd> {
                 .filter(|arg_id| matcher.check_explicit(arg_id, &ArgPredicate::IsPresent))
                 .count();
             if num_user_values == 0 {
-                let message = self.cmd.write_help_err(false, Stream::Stderr);
+                let message = self.cmd.write_help_err(false);
                 return Err(Error::display_help_error(self.cmd, message));
             }
         }
diff --git a/src/util/flat_map.rs b/src/util/flat_map.rs
index 9b709d29..d8f85bb2 100644
--- a/src/util/flat_map.rs
+++ b/src/util/flat_map.rs
@@ -22,9 +22,19 @@ impl<K: PartialEq + Eq, V> FlatMap<K, V> {
             }
         }
 
+        self.insert_unchecked(key, value);
+        None
+    }
+
+    pub(crate) fn insert_unchecked(&mut self, key: K, value: V) {
         self.keys.push(key);
         self.values.push(value);
-        None
+    }
+
+    pub(crate) fn extend_unchecked(&mut self, iter: impl IntoIterator<Item = (K, V)>) {
+        for (key, value) in iter {
+            self.insert_unchecked(key, value);
+        }
     }
 
     pub fn contains_key<Q: ?Sized>(&self, key: &Q) -> bool
@@ -97,6 +107,13 @@ impl<K: PartialEq + Eq, V> FlatMap<K, V> {
         self.keys.iter()
     }
 
+    pub fn iter(&self) -> Iter<K, V> {
+        Iter {
+            keys: self.keys.iter(),
+            values: self.values.iter(),
+        }
+    }
+
     pub fn iter_mut(&mut self) -> IterMut<K, V> {
         IterMut {
             keys: self.keys.iter_mut(),
@@ -153,6 +170,42 @@ pub struct OccupiedEntry<'a, K: 'a, V: 'a> {
     index: usize,
 }
 
+pub struct Iter<'a, K: 'a, V: 'a> {
+    keys: std::slice::Iter<'a, K>,
+    values: std::slice::Iter<'a, V>,
+}
+
+impl<'a, K, V> Iterator for Iter<'a, K, V> {
+    type Item = (&'a K, &'a V);
+
+    fn next(&mut self) -> Option<(&'a K, &'a V)> {
+        match self.keys.next() {
+            Some(k) => {
+                let v = self.values.next().unwrap();
+                Some((k, v))
+            }
+            None => None,
+        }
+    }
+    fn size_hint(&self) -> (usize, Option<usize>) {
+        self.keys.size_hint()
+    }
+}
+
+impl<'a, K, V> DoubleEndedIterator for Iter<'a, K, V> {
+    fn next_back(&mut self) -> Option<(&'a K, &'a V)> {
+        match self.keys.next_back() {
+            Some(k) => {
+                let v = self.values.next_back().unwrap();
+                Some((k, v))
+            }
+            None => None,
+        }
+    }
+}
+
+impl<'a, K, V> ExactSizeIterator for Iter<'a, K, V> {}
+
 pub struct IterMut<'a, K: 'a, V: 'a> {
     keys: std::slice::IterMut<'a, K>,
     values: std::slice::IterMut<'a, V>,


Test patch:
diff --git a/tests/builder/error.rs b/tests/builder/error.rs
index 464e6bae..e2f95640 100644
--- a/tests/builder/error.rs
+++ b/tests/builder/error.rs
@@ -3,7 +3,12 @@ use super::utils;
 use clap::{arg, error::ErrorKind, value_parser, Arg, Command, Error};
 
 #[track_caller]
-fn assert_error(err: Error, expected_kind: ErrorKind, expected_output: &str, stderr: bool) {
+fn assert_error<F: clap::error::ErrorFormatter>(
+    err: Error<F>,
+    expected_kind: ErrorKind,
+    expected_output: &str,
+    stderr: bool,
+) {
     let actual_output = err.to_string();
     assert_eq!(
         stderr,
@@ -73,3 +78,95 @@ fn value_validation_has_newline() {
         err.to_string()
     );
 }
+
+#[test]
+fn null_prints_help() {
+    let cmd = Command::new("test");
+    let res = cmd
+        .try_get_matches_from(["test", "--help"])
+        .map_err(|e| e.apply::<clap::error::NullFormatter>());
+    assert!(res.is_err());
+    let err = res.unwrap_err();
+    let expected_kind = ErrorKind::DisplayHelp;
+    static MESSAGE: &str = "\
+test 
+
+USAGE:
+    test
+
+OPTIONS:
+    -h, --help    Print help information
+";
+    assert_error(err, expected_kind, MESSAGE, false);
+}
+
+#[test]
+fn raw_prints_help() {
+    let cmd = Command::new("test");
+    let res = cmd
+        .try_get_matches_from(["test", "--help"])
+        .map_err(|e| e.apply::<clap::error::RawFormatter>());
+    assert!(res.is_err());
+    let err = res.unwrap_err();
+    let expected_kind = ErrorKind::DisplayHelp;
+    static MESSAGE: &str = "\
+test 
+
+USAGE:
+    test
+
+OPTIONS:
+    -h, --help    Print help information
+";
+    assert_error(err, expected_kind, MESSAGE, false);
+}
+
+#[test]
+fn null_ignores_validation_error() {
+    let cmd = Command::new("test");
+    let res = cmd
+        .try_get_matches_from(["test", "unused"])
+        .map_err(|e| e.apply::<clap::error::NullFormatter>());
+    assert!(res.is_err());
+    let err = res.unwrap_err();
+    let expected_kind = ErrorKind::UnknownArgument;
+    static MESSAGE: &str = "";
+    assert_error(err, expected_kind, MESSAGE, true);
+}
+
+#[test]
+fn rich_formats_validation_error() {
+    let cmd = Command::new("test");
+    let res = cmd.try_get_matches_from(["test", "unused"]);
+    assert!(res.is_err());
+    let err = res.unwrap_err();
+    let expected_kind = ErrorKind::UnknownArgument;
+    static MESSAGE: &str = "\
+error: Found argument 'unused' which wasn't expected, or isn't valid in this context
+
+USAGE:
+    test
+
+For more information try --help
+";
+    assert_error(err, expected_kind, MESSAGE, true);
+}
+
+#[test]
+fn raw_formats_validation_error() {
+    let cmd = Command::new("test");
+    let res = cmd
+        .try_get_matches_from(["test", "unused"])
+        .map_err(|e| e.apply::<clap::error::RawFormatter>());
+    assert!(res.is_err());
+    let err = res.unwrap_err();
+    let expected_kind = ErrorKind::UnknownArgument;
+    static MESSAGE: &str = "\
+error: Found an argument which wasn't expected or isn't valid in this context
+
+Invalid Argument: unused
+USAGE:
+    test
+";
+    assert_error(err, expected_kind, MESSAGE, true);
+}