Add custom section support for reports (#18)

This commit is contained in:
Jane Lusby 2020-05-25 19:39:26 -07:00 committed by GitHub
parent 9f5defedb7
commit 7b2d9f4218
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 1066 additions and 272 deletions

View File

@ -29,6 +29,7 @@ once_cell = "1.4.0"
tracing-subscriber = "0.2.5" tracing-subscriber = "0.2.5"
tracing = "0.1.13" tracing = "0.1.13"
pretty_assertions = "0.6.1" pretty_assertions = "0.6.1"
thiserror = "1.0.19"
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true

162
README.md
View File

@ -11,8 +11,14 @@
[docs-badge]: https://img.shields.io/badge/docs-latest-blue.svg [docs-badge]: https://img.shields.io/badge/docs-latest-blue.svg
[docs-url]: https://docs.rs/color-eyre [docs-url]: https://docs.rs/color-eyre
A custom context for the [`eyre`] crate for colorful error reports, suggestions, A custom context for the [`eyre`] crate for colorful error reports with suggestions, custom
and [`tracing-error`] support. sections, [`tracing-error`] support, and backtraces on stable.
## TLDR
`color_eyre` helps you build error reports that look like this:
![custom section example](https://raw.githubusercontent.com/yaahc/color-eyre/master/pictures/custom_section.png)
## Setup ## Setup
@ -48,17 +54,33 @@ eyre = "0.4"
color-eyre = { version = "0.3", default-features = false } color-eyre = { version = "0.3", default-features = false }
``` ```
## Example ## Features
### Multiple report format verbosity levels
`color-eyre` provides 3 different report formats for how it formats the captured `SpanTrace`
and `Backtrace`, minimal, short, and full. Take the following example, taken from
[`examples/usage.rs`]:
```rust ```rust
use color_eyre::{Help, Report}; use color_eyre::{Help, Report};
use eyre::WrapErr; use eyre::WrapErr;
use tracing::{info, instrument}; use tracing::{info, instrument};
use tracing_error::ErrorLayer;
use tracing_subscriber::prelude::*;
use tracing_subscriber::{fmt, EnvFilter};
#[instrument]
fn main() -> Result<(), Report> { fn main() -> Result<(), Report> {
#[cfg(feature = "capture-spantrace")]
install_tracing();
Ok(read_config()?)
}
#[cfg(feature = "capture-spantrace")]
fn install_tracing() {
use tracing_error::ErrorLayer;
use tracing_subscriber::prelude::*;
use tracing_subscriber::{fmt, EnvFilter};
let fmt_layer = fmt::layer().with_target(false); let fmt_layer = fmt::layer().with_target(false);
let filter_layer = EnvFilter::try_from_default_env() let filter_layer = EnvFilter::try_from_default_env()
.or_else(|_| EnvFilter::try_new("info")) .or_else(|_| EnvFilter::try_new("info"))
@ -69,8 +91,6 @@ fn main() -> Result<(), Report> {
.with(fmt_layer) .with(fmt_layer)
.with(ErrorLayer::default()) .with(ErrorLayer::default())
.init(); .init();
Ok(read_config()?)
} }
#[instrument] #[instrument]
@ -87,17 +107,112 @@ fn read_config() -> Result<(), Report> {
} }
``` ```
## Minimal Report Format ---
![minimal report format](./pictures/minimal.png) Running `cargo run --example usage` without `RUST_LIB_BACKTRACE` set will produce a minimal
report like this:
## Short Report Format (with `RUST_LIB_BACKTRACE=1`) ![minimal report format](https://raw.githubusercontent.com/yaahc/color-eyre/master/pictures/minimal.png)
![short report format](./pictures/short.png) <br>
## Full Report Format (with `RUST_LIB_BACKTRACE=full`) Running `RUST_LIB_BACKTRACE=1 cargo run --example usage` tells `color-eyre` to use the short
format, which additionally capture a [`backtrace::Backtrace`]:
![full report format](./pictures/full.png) ![short report format](https://raw.githubusercontent.com/yaahc/color-eyre/master/pictures/short.png)
<br>
Finally, running `RUST_LIB_BACKTRACE=full cargo run --example usage` tells `color-eyre` to use
the full format, which in addition to the above will attempt to include source lines where the
error originated from, assuming it can find them on the disk.
![full report format](https://raw.githubusercontent.com/yaahc/color-eyre/master/pictures/full.png)
### Custom `Section`s for error reports via [`Help`] trait
The `section` module provides helpers for adding extra sections to error reports. Sections are
disinct from error messages and are displayed independently from the chain of errors. Take this
example of adding sections to contain `stderr` and `stdout` from a failed command, taken from
[`examples/custom_section.rs`]:
```rust
use color_eyre::{SectionExt, Help, Report};
use eyre::eyre;
use std::process::Command;
use tracing::instrument;
trait Output {
fn output2(&mut self) -> Result<String, Report>;
}
impl Output for Command {
#[instrument]
fn output2(&mut self) -> Result<String, Report> {
let output = self.output()?;
let stdout = String::from_utf8_lossy(&output.stdout);
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
Err(eyre!("cmd exited with non-zero status code"))
.with_section(move || {
"Stdout:"
.skip_if(|| stdout.is_empty())
.body(stdout.trim().to_string())
})
.with_section(move || {
"Stderr:"
.skip_if(|| stderr.is_empty())
.body(stderr.trim().to_string())
})
} else {
Ok(stdout.into())
}
}
}
```
---
Here we have an function that, if the command exits unsuccessfully, creates a report indicating
the failure and attaches two sections, one for `stdout` and one for `stderr`. Each section
includes a short header and a body that contains the actual output. Additionally these sections
use `skip_if` to tell the report not to include them if there was no output, preventing empty
sections from polluting the end report.
Running `cargo run --example custom_section` shows us how these sections are included in the
output:
![custom section example](https://raw.githubusercontent.com/yaahc/color-eyre/master/pictures/custom_section.png)
Only the `Stderr:` section actually gets included. The `cat` command fails, so stdout ends up
being empty and is skipped in the final report. This gives us a short and concise error report
indicating exactly what was attempted and how it failed.
### Aggregating multiple errors into one report
It's not uncommon for programs like batched task runners or parsers to want to
return an error with multiple sources. The current version of the error trait
does not support this use case very well, though there is [work being
done](https://github.com/rust-lang/rfcs/pull/2895) to improve this.
For now however one way to work around this is to compose errors outside the
error trait. `color-eyre` supports such composition in its error reports via
the `Help` trait.
For an example of how to aggregate errors check out [`examples/multiple_errors.rs`].
### Custom configuration for `color-backtrace` for setting custom filters and more
The pretty printing for backtraces and span traces isn't actually provided by `color-eyre`, but
instead comes from its dependencies [`color-backtrace`] and [`color-spantrace`].
`color-backtrace` in particular has many more features than are exported by `color-eyre`, such
as customized color schemes, panic hooks, and custom frame filters. The custom frame filters
are particularly useful when combined with `color-eyre`, so to enable their usage we provide
the `install` fn for setting up a custom `BacktracePrinter` with custom filters installed.
For an example of how to setup custom filters, check out [`examples/custom_filter.rs`].
## Explanation ## Explanation
@ -112,17 +227,7 @@ pub type Report = eyre::Report<Context>;
pub type Result<T, E = Report> = core::result::Result<T, E>; pub type Result<T, E = Report> = core::result::Result<T, E>;
``` ```
## Features Please refer to the [`Context`] type's docs for more details about its feature set.
- captures a [`backtrace::Backtrace`] and prints using [`color-backtrace`]
- captures a [`tracing_error::SpanTrace`] and prints using
[`color-spantrace`]
- Only capture SpanTrace by default for better performance.
- display source lines when `RUST_LIB_BACKTRACE=full` is set
- store help text via [`Help`] trait and display after final report
- custom `color-backtrace` configuration via `color_eyre::install`, such as
adding custom frame filters
[`eyre`]: https://docs.rs/eyre [`eyre`]: https://docs.rs/eyre
[`tracing-error`]: https://docs.rs/tracing-error [`tracing-error`]: https://docs.rs/tracing-error
@ -131,9 +236,14 @@ pub type Result<T, E = Report> = core::result::Result<T, E>;
[`backtrace::Backtrace`]: https://docs.rs/backtrace/*/backtrace/struct.Backtrace.html [`backtrace::Backtrace`]: https://docs.rs/backtrace/*/backtrace/struct.Backtrace.html
[`tracing_error::SpanTrace`]: https://docs.rs/tracing-error/*/tracing_error/struct.SpanTrace.html [`tracing_error::SpanTrace`]: https://docs.rs/tracing-error/*/tracing_error/struct.SpanTrace.html
[`color-spantrace`]: https://github.com/yaahc/color-spantrace [`color-spantrace`]: https://github.com/yaahc/color-spantrace
[`Help`]: trait.Help.html [`Help`]: https://docs.rs/color-eyre/*/color_eyre/trait.Help.html
[`eyre::Report`]: https://docs.rs/eyre/*/eyre/struct.Report.html [`eyre::Report`]: https://docs.rs/eyre/*/eyre/struct.Report.html
[`eyre::Result`]: https://docs.rs/eyre/*/eyre/type.Result.html [`eyre::Result`]: https://docs.rs/eyre/*/eyre/type.Result.html
[`Context`]: https://docs.rs/color-eyre/*/color_eyre/struct.Context.html
[`examples/usage.rs`]: https://github.com/yaahc/color-eyre/blob/master/examples/usage.rs
[`examples/custom_filter.rs`]: https://github.com/yaahc/color-eyre/blob/master/examples/custom_filter.rs
[`examples/custom_section.rs`]: https://github.com/yaahc/color-eyre/blob/master/examples/custom_section.rs
[`examples/multiple_errors.rs`]: https://github.com/yaahc/color-eyre/blob/master/examples/multiple_errors.rs
#### License #### License

View File

@ -0,0 +1,72 @@
use color_eyre::{Help, Report, SectionExt};
use eyre::{eyre, WrapErr};
use std::process::Command;
use tracing::instrument;
trait Output {
fn output2(&mut self) -> Result<String, Report>;
}
impl Output for Command {
#[instrument]
fn output2(&mut self) -> Result<String, Report> {
let output = self.output()?;
let stdout = String::from_utf8_lossy(&output.stdout);
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
Err(eyre!("cmd exited with non-zero status code"))
.with_section(move || {
"Stdout:"
.skip_if(|| stdout.is_empty())
.body(stdout.trim().to_string())
})
.with_section(move || {
"Stderr:"
.skip_if(|| stderr.is_empty())
.body(stderr.trim().to_string())
})
} else {
Ok(stdout.into())
}
}
}
#[instrument]
fn main() -> Result<(), Report> {
#[cfg(feature = "capture-spantrace")]
install_tracing();
Ok(read_config().map(drop)?)
}
#[cfg(feature = "capture-spantrace")]
fn install_tracing() {
use tracing_error::ErrorLayer;
use tracing_subscriber::prelude::*;
use tracing_subscriber::{fmt, EnvFilter};
let fmt_layer = fmt::layer().with_target(false);
let filter_layer = EnvFilter::try_from_default_env()
.or_else(|_| EnvFilter::try_new("info"))
.unwrap();
tracing_subscriber::registry()
.with(filter_layer)
.with(fmt_layer)
.with(ErrorLayer::default())
.init();
}
#[instrument]
fn read_file(path: &str) -> Result<String, Report> {
Command::new("cat").arg("fake_file").output2()
}
#[instrument]
fn read_config() -> Result<String, Report> {
read_file("fake_file")
.wrap_err("Unable to read config")
.suggestion("try using a file that exists next time")
}

View File

@ -0,0 +1,53 @@
use color_eyre::{Help, Report};
use eyre::eyre;
use thiserror::Error;
fn main() -> Result<(), Report> {
let errors = get_errors();
join_errors(errors)
}
fn join_errors(results: Vec<Result<(), SourceError>>) -> Result<(), Report> {
if results.iter().all(|r| r.is_ok()) {
return Ok(());
}
results
.into_iter()
.filter(Result::is_err)
.map(Result::unwrap_err)
.fold(Err(eyre!("encountered multiple errors")), |report, e| {
report.error(e)
})
}
/// Helper function to generate errors
fn get_errors() -> Vec<Result<(), SourceError>> {
vec![
Err(SourceError {
source: StrError("The task you ran encountered an error"),
msg: "The task could not be completed",
}),
Err(SourceError {
source: StrError("The machine you're connecting to is actively on fire"),
msg: "The machine is unreachable",
}),
Err(SourceError {
source: StrError("The file you're parsing is literally written in c++ instead of rust, what the hell"),
msg: "The file could not be parsed",
}),
]
}
/// Arbitrary error type for demonstration purposes
#[derive(Debug, Error)]
#[error("{0}")]
struct StrError(&'static str);
/// Arbitrary error type for demonstration purposes with a source error
#[derive(Debug, Error)]
#[error("{msg}")]
struct SourceError {
msg: &'static str,
source: StrError,
}

BIN
pictures/custom_section.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

View File

@ -1,218 +0,0 @@
use crate::{Report, Result};
use ansi_term::Color::*;
use std::fmt::{self, Display};
/// A helper trait for attaching help text to errors to be displayed after the chain of errors
pub trait Help<T>: private::Sealed {
/// Add a note to an error, to be displayed after the chain of errors.
///
/// # Examples
///
/// ```rust
/// # use std::{error::Error, fmt::{self, Display}};
/// # use color_eyre::Result;
/// # #[derive(Debug)]
/// # struct FakeErr;
/// # impl Display for FakeErr {
/// # fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
/// # write!(f, "FakeErr")
/// # }
/// # }
/// # impl std::error::Error for FakeErr {}
/// # fn main() -> Result<()> {
/// # fn fallible_fn() -> Result<(), FakeErr> {
/// # Ok(())
/// # }
/// use color_eyre::Help as _;
///
/// fallible_fn().note("This might have failed due to ...")?;
/// # Ok(())
/// # }
/// ```
fn note<C>(self, context: C) -> Result<T>
where
C: Display + Send + Sync + 'static;
/// Add a Note to an error, to be displayed after the chain of errors, which is lazily
/// evaluated only in the case of an error.
///
/// # Examples
///
/// ```rust
/// # use std::{error::Error, fmt::{self, Display}};
/// # use color_eyre::Result;
/// # #[derive(Debug)]
/// # struct FakeErr;
/// # impl Display for FakeErr {
/// # fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
/// # write!(f, "FakeErr")
/// # }
/// # }
/// # impl std::error::Error for FakeErr {}
/// # fn main() -> Result<()> {
/// # fn fallible_fn() -> Result<(), FakeErr> {
/// # Ok(())
/// # }
/// use color_eyre::Help as _;
///
/// fallible_fn().with_note(|| {
/// format!("This might have failed due to ... It has failed {} times", 100)
/// })?;
/// # Ok(())
/// # }
/// ```
fn with_note<C, F>(self, f: F) -> Result<T>
where
C: Display + Send + Sync + 'static,
F: FnOnce() -> C;
/// Add a Warning to an error, to be displayed after the chain of errors.
fn warning<C>(self, context: C) -> Result<T>
where
C: Display + Send + Sync + 'static;
/// Add a Warning to an error, to be displayed after the chain of errors, which is lazily
/// evaluated only in the case of an error.
fn with_warning<C, F>(self, f: F) -> Result<T>
where
C: Display + Send + Sync + 'static,
F: FnOnce() -> C;
/// Add a Suggestion to an error, to be displayed after the chain of errors.
fn suggestion<C>(self, context: C) -> Result<T>
where
C: Display + Send + Sync + 'static;
/// Add a Suggestion to an error, to be displayed after the chain of errors, which is lazily
/// evaluated only in the case of an error.
fn with_suggestion<C, F>(self, f: F) -> Result<T>
where
C: Display + Send + Sync + 'static,
F: FnOnce() -> C;
}
impl<T, E> Help<T> for std::result::Result<T, E>
where
E: Into<Report>,
{
fn note<C>(self, context: C) -> Result<T>
where
C: Display + Send + Sync + 'static,
{
self.map_err(|e| {
let mut e = e.into();
e.context_mut().help.push(HelpInfo::Note(Box::new(context)));
e
})
}
fn with_note<C, F>(self, context: F) -> Result<T>
where
C: Display + Send + Sync + 'static,
F: FnOnce() -> C,
{
self.map_err(|e| {
let mut e = e.into();
e.context_mut()
.help
.push(HelpInfo::Note(Box::new(context())));
e
})
}
fn warning<C>(self, context: C) -> Result<T>
where
C: Display + Send + Sync + 'static,
{
self.map_err(|e| {
let mut e = e.into();
e.context_mut()
.help
.push(HelpInfo::Warning(Box::new(context)));
e
})
}
fn with_warning<C, F>(self, context: F) -> Result<T>
where
C: Display + Send + Sync + 'static,
F: FnOnce() -> C,
{
self.map_err(|e| {
let mut e = e.into();
e.context_mut()
.help
.push(HelpInfo::Warning(Box::new(context())));
e
})
}
fn suggestion<C>(self, context: C) -> Result<T>
where
C: Display + Send + Sync + 'static,
{
self.map_err(|e| {
let mut e = e.into();
e.context_mut()
.help
.push(HelpInfo::Suggestion(Box::new(context)));
e
})
}
fn with_suggestion<C, F>(self, context: F) -> Result<T>
where
C: Display + Send + Sync + 'static,
F: FnOnce() -> C,
{
self.map_err(|e| {
let mut e = e.into();
e.context_mut()
.help
.push(HelpInfo::Suggestion(Box::new(context())));
e
})
}
}
pub(crate) enum HelpInfo {
Note(Box<dyn Display + Send + Sync + 'static>),
Warning(Box<dyn Display + Send + Sync + 'static>),
Suggestion(Box<dyn Display + Send + Sync + 'static>),
}
impl Display for HelpInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Note(context) => write!(f, "{}: {}", Cyan.paint("Note"), context),
Self::Warning(context) => write!(f, "{}: {}", Yellow.paint("Warning"), context),
Self::Suggestion(context) => write!(f, "{}: {}", Cyan.paint("Suggestion"), context),
}
}
}
impl fmt::Debug for HelpInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Note(context) => f
.debug_tuple("Note")
.field(&format_args!("{}", context))
.finish(),
Self::Warning(context) => f
.debug_tuple("Warning")
.field(&format_args!("{}", context))
.finish(),
Self::Suggestion(context) => f
.debug_tuple("Suggestion")
.field(&format_args!("{}", context))
.finish(),
}
}
}
pub(crate) mod private {
use crate::Report;
pub trait Sealed {}
impl<T, E> Sealed for std::result::Result<T, E> where E: Into<Report> {}
}

View File

@ -1,5 +1,11 @@
//! A custom context for the [`eyre`] crate for colorful error reports, suggestions, //! A custom context for the [`eyre`] crate for colorful error reports with suggestions, custom
//! and [`tracing-error`] support. //! sections, [`tracing-error`] support, and backtraces on stable.
//!
//! ## TLDR
//!
//! `color_eyre` helps you build error reports that look like this:
//!
//! ![custom section example](https://raw.githubusercontent.com/yaahc/color-eyre/master/pictures/custom_section.png)
//! //!
//! ## Setup //! ## Setup
//! //!
@ -35,7 +41,13 @@
//! color-eyre = { version = "0.3", default-features = false } //! color-eyre = { version = "0.3", default-features = false }
//! ``` //! ```
//! //!
//! ## Example //! ## Features
//!
//! ### Multiple report format verbosity levels
//!
//! `color-eyre` provides 3 different report formats for how it formats the captured `SpanTrace`
//! and `Backtrace`, minimal, short, and full. Take the following example, taken from
//! [`examples/usage.rs`]:
//! //!
//! ```rust,should_panic //! ```rust,should_panic
//! use color_eyre::{Help, Report}; //! use color_eyre::{Help, Report};
@ -82,18 +94,113 @@
//! } //! }
//! ``` //! ```
//! //!
//! ## Minimal Report Format //! ---
//!
//! Running `cargo run --example usage` without `RUST_LIB_BACKTRACE` set will produce a minimal
//! report like this:
//! //!
//! ![minimal report format](https://raw.githubusercontent.com/yaahc/color-eyre/master/pictures/minimal.png) //! ![minimal report format](https://raw.githubusercontent.com/yaahc/color-eyre/master/pictures/minimal.png)
//! //!
//! ## Short Report Format (with `RUST_LIB_BACKTRACE=1`) //! <br>
//!
//! Running `RUST_LIB_BACKTRACE=1 cargo run --example usage` tells `color-eyre` to use the short
//! format, which additionally capture a [`backtrace::Backtrace`]:
//! //!
//! ![short report format](https://raw.githubusercontent.com/yaahc/color-eyre/master/pictures/short.png) //! ![short report format](https://raw.githubusercontent.com/yaahc/color-eyre/master/pictures/short.png)
//! //!
//! ## Full Report Format (with `RUST_LIB_BACKTRACE=full`) //! <br>
//!
//! Finally, running `RUST_LIB_BACKTRACE=full cargo run --example usage` tells `color-eyre` to use
//! the full format, which in addition to the above will attempt to include source lines where the
//! error originated from, assuming it can find them on the disk.
//! //!
//! ![full report format](https://raw.githubusercontent.com/yaahc/color-eyre/master/pictures/full.png) //! ![full report format](https://raw.githubusercontent.com/yaahc/color-eyre/master/pictures/full.png)
//! //!
//! ### Custom `Section`s for error reports via [`Help`] trait
//!
//! The `section` module provides helpers for adding extra sections to error reports. Sections are
//! disinct from error messages and are displayed independently from the chain of errors. Take this
//! example of adding sections to contain `stderr` and `stdout` from a failed command, taken from
//! [`examples/custom_section.rs`]:
//!
//! ```rust
//! use color_eyre::{SectionExt, Help, Report};
//! use eyre::eyre;
//! use std::process::Command;
//! use tracing::instrument;
//!
//! trait Output {
//! fn output2(&mut self) -> Result<String, Report>;
//! }
//!
//! impl Output for Command {
//! #[instrument]
//! fn output2(&mut self) -> Result<String, Report> {
//! let output = self.output()?;
//!
//! let stdout = String::from_utf8_lossy(&output.stdout);
//!
//! if !output.status.success() {
//! let stderr = String::from_utf8_lossy(&output.stderr);
//! Err(eyre!("cmd exited with non-zero status code"))
//! .with_section(move || {
//! "Stdout:"
//! .skip_if(|| stdout.is_empty())
//! .body(stdout.trim().to_string())
//! })
//! .with_section(move || {
//! "Stderr:"
//! .skip_if(|| stderr.is_empty())
//! .body(stderr.trim().to_string())
//! })
//! } else {
//! Ok(stdout.into())
//! }
//! }
//! }
//! ```
//!
//! ---
//!
//! Here we have an function that, if the command exits unsuccessfully, creates a report indicating
//! the failure and attaches two sections, one for `stdout` and one for `stderr`. Each section
//! includes a short header and a body that contains the actual output. Additionally these sections
//! use `skip_if` to tell the report not to include them if there was no output, preventing empty
//! sections from polluting the end report.
//!
//! Running `cargo run --example custom_section` shows us how these sections are included in the
//! output:
//!
//! ![custom section example](https://raw.githubusercontent.com/yaahc/color-eyre/master/pictures/custom_section.png)
//!
//! Only the `Stderr:` section actually gets included. The `cat` command fails, so stdout ends up
//! being empty and is skipped in the final report. This gives us a short and concise error report
//! indicating exactly what was attempted and how it failed.
//!
//! ### Aggregating multiple errors into one report
//!
//! It's not uncommon for programs like batched task runners or parsers to want to
//! return an error with multiple sources. The current version of the error trait
//! does not support this use case very well, though there is [work being
//! done](https://github.com/rust-lang/rfcs/pull/2895) to improve this.
//!
//! For now however one way to work around this is to compose errors outside the
//! error trait. `color-eyre` supports such composition in its error reports via
//! the `Help` trait.
//!
//! For an example of how to aggregate errors check out [`examples/multiple_errors.rs`].
//!
//! ### Custom configuration for `color-backtrace` for setting custom filters and more
//!
//! The pretty printing for backtraces and span traces isn't actually provided by `color-eyre`, but
//! instead comes from its dependencies [`color-backtrace`] and [`color-spantrace`].
//! `color-backtrace` in particular has many more features than are exported by `color-eyre`, such
//! as customized color schemes, panic hooks, and custom frame filters. The custom frame filters
//! are particularly useful when combined with `color-eyre`, so to enable their usage we provide
//! the `install` fn for setting up a custom `BacktracePrinter` with custom filters installed.
//!
//! For an example of how to setup custom filters, check out [`examples/custom_filter.rs`].
//!
//! ## Explanation //! ## Explanation
//! //!
//! This crate works by defining a `Context` type which implements [`eyre::EyreContext`] //! This crate works by defining a `Context` type which implements [`eyre::EyreContext`]
@ -109,18 +216,6 @@
//! //!
//! Please refer to the [`Context`] type's docs for more details about its feature set. //! Please refer to the [`Context`] type's docs for more details about its feature set.
//! //!
//! ## Features
//!
//! - captures a [`backtrace::Backtrace`] and prints using [`color-backtrace`]
//! - captures a [`tracing_error::SpanTrace`] and prints using
//! [`color-spantrace`]
//! - Only capture SpanTrace by default for better performance.
//! - display source lines when `RUST_LIB_BACKTRACE=full` is set
//! - store help text via [`Help`] trait and display after final report
//! - custom `color-backtrace` configuration via `color_eyre::install`,
//! such as adding custom filters
//!
//!
//! [`eyre`]: https://docs.rs/eyre //! [`eyre`]: https://docs.rs/eyre
//! [`tracing-error`]: https://docs.rs/tracing-error //! [`tracing-error`]: https://docs.rs/tracing-error
//! [`color-backtrace`]: https://docs.rs/color-backtrace //! [`color-backtrace`]: https://docs.rs/color-backtrace
@ -128,10 +223,14 @@
//! [`backtrace::Backtrace`]: https://docs.rs/backtrace/*/backtrace/struct.Backtrace.html //! [`backtrace::Backtrace`]: https://docs.rs/backtrace/*/backtrace/struct.Backtrace.html
//! [`tracing_error::SpanTrace`]: https://docs.rs/tracing-error/*/tracing_error/struct.SpanTrace.html //! [`tracing_error::SpanTrace`]: https://docs.rs/tracing-error/*/tracing_error/struct.SpanTrace.html
//! [`color-spantrace`]: https://github.com/yaahc/color-spantrace //! [`color-spantrace`]: https://github.com/yaahc/color-spantrace
//! [`Help`]: trait.Help.html //! [`Help`]: https://docs.rs/color-eyre/*/color_eyre/trait.Help.html
//! [`eyre::Report`]: https://docs.rs/eyre/*/eyre/struct.Report.html //! [`eyre::Report`]: https://docs.rs/eyre/*/eyre/struct.Report.html
//! [`eyre::Result`]: https://docs.rs/eyre/*/eyre/type.Result.html //! [`eyre::Result`]: https://docs.rs/eyre/*/eyre/type.Result.html
//! [`Context`]: struct.Context.html //! [`Context`]: https://docs.rs/color-eyre/*/color_eyre/struct.Context.html
//! [`examples/usage.rs`]: https://github.com/yaahc/color-eyre/blob/master/examples/usage.rs
//! [`examples/custom_filter.rs`]: https://github.com/yaahc/color-eyre/blob/master/examples/custom_filter.rs
//! [`examples/custom_section.rs`]: https://github.com/yaahc/color-eyre/blob/master/examples/custom_section.rs
//! [`examples/multiple_errors.rs`]: https://github.com/yaahc/color-eyre/blob/master/examples/multiple_errors.rs
#![doc(html_root_url = "https://docs.rs/color-eyre/0.3.2")] #![doc(html_root_url = "https://docs.rs/color-eyre/0.3.2")]
#![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))]
#![warn( #![warn(
@ -161,10 +260,10 @@ use ansi_term::Color::*;
use backtrace::Backtrace; use backtrace::Backtrace;
pub use color_backtrace::BacktracePrinter; pub use color_backtrace::BacktracePrinter;
use eyre::*; use eyre::*;
pub use help::Help;
use help::HelpInfo;
use indenter::{indented, Format}; use indenter::{indented, Format};
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use section::Order;
pub use section::{help::Help, Section, SectionExt};
#[cfg(feature = "capture-spantrace")] #[cfg(feature = "capture-spantrace")]
use std::error::Error; use std::error::Error;
use std::{ use std::{
@ -175,7 +274,7 @@ use std::{
#[cfg(feature = "capture-spantrace")] #[cfg(feature = "capture-spantrace")]
use tracing_error::{ExtractSpanTrace, SpanTrace, SpanTraceStatus}; use tracing_error::{ExtractSpanTrace, SpanTrace, SpanTraceStatus};
mod help; pub mod section;
static CONFIG: OnceCell<BacktracePrinter> = OnceCell::new(); static CONFIG: OnceCell<BacktracePrinter> = OnceCell::new();
@ -194,7 +293,7 @@ pub struct Context {
backtrace: Option<Backtrace>, backtrace: Option<Backtrace>,
#[cfg(feature = "capture-spantrace")] #[cfg(feature = "capture-spantrace")]
span_trace: Option<SpanTrace>, span_trace: Option<SpanTrace>,
help: Vec<HelpInfo>, sections: Vec<Section>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -318,7 +417,7 @@ impl EyreContext for Context {
backtrace, backtrace,
#[cfg(feature = "capture-spantrace")] #[cfg(feature = "capture-spantrace")]
span_trace, span_trace,
help: Vec::new(), sections: Vec::new(),
} }
} }
@ -347,6 +446,22 @@ impl EyreContext for Context {
write!(indented(f).ind(n), "{}", Red.paint(&buf))?; write!(indented(f).ind(n), "{}", Red.paint(&buf))?;
} }
for section in self
.sections
.iter()
.filter(|s| matches!(s.order, Order::AfterErrMsgs))
{
write!(f, "\n\n{:?}", section)?;
}
for section in self
.sections
.iter()
.filter(|s| matches!(s.order, Order::BeforeSpanTrace))
{
write!(f, "\n\n{:?}", section)?;
}
#[cfg(feature = "capture-spantrace")] #[cfg(feature = "capture-spantrace")]
{ {
let span_trace = self let span_trace = self
@ -378,12 +493,20 @@ impl EyreContext for Context {
"{}", "{}",
bt_str bt_str
)?; )?;
} else if !self.help.is_empty() { } else if self
.sections
.iter()
.any(|s| matches!(s.order, Order::AfterBackTrace))
{
writeln!(f)?; writeln!(f)?;
} }
for help in &self.help { for section in self
write!(f, "\n{}", help)?; .sections
.iter()
.filter(|s| matches!(s.order, Order::AfterBackTrace))
{
write!(f, "\n{:?}", section)?;
} }
Ok(()) Ok(())

405
src/section/help.rs Normal file
View File

@ -0,0 +1,405 @@
//! Provides an extension trait for attaching `Section`s to error reports.
use crate::{section, Report, Result, Section};
use ansi_term::Color::*;
use std::fmt::{self, Display};
/// A helper trait for attaching help text to errors to be displayed after the chain of errors
///
/// `color_eyre` provides two types of help text that can be attached to error reports: custom
/// sections and pre-configured sections. Custom sections are added via the `section` and
/// `with_section` methods, and give maximum control over formatting. For more details check out
/// the docs for [`Section`].
///
/// The pre-configured sections are provided via `suggestion`, `warning`, and `note`. These
/// sections are displayed after all other sections with no extra newlines between subsequent Help
/// sections. They consist only of a header portion and are prepended with a colored string
/// indicating the kind of section, e.g. `Note: This might have failed due to ..."
///
/// [`Section`]: struct.Section.html
pub trait Help<T>: private::Sealed {
/// Add a section to an error report, to be displayed after the chain of errors.
///
/// Sections are displayed in the order they are added to the error report. They are displayed
/// immediately after the `Error:` section and before the `SpanTrace` and `Backtrace` sections.
/// They consist of a header and an optional body. The body of the section is indented by
/// default.
///
/// # Examples
///
/// ```rust,should_panic
/// use color_eyre::{Report, Help};
/// use eyre::eyre;
///
/// Err(eyre!("command failed"))
/// .section("Please report bugs to https://real.url/bugs")?;
/// # Ok::<_, Report>(())
/// ```
fn section<C>(self, section: C) -> Result<T>
where
C: Into<Section>;
/// Add a Section to an error report, to be displayed after the chain of errors. The closure to
/// create the Section is lazily evaluated only in the case of an error.
///
/// # Examples
///
/// ```rust
/// use color_eyre::{Report, Help, SectionExt};
/// use eyre::eyre;
///
/// let output = std::process::Command::new("ls")
/// .output()?;
///
/// let output = if !output.status.success() {
/// let stderr = String::from_utf8_lossy(&output.stderr);
/// Err(eyre!("cmd exited with non-zero status code"))
/// .with_section(move || {
/// "Stderr:"
/// .skip_if(|| stderr.is_empty())
/// .body(stderr.trim().to_string())
/// })?
/// } else {
/// String::from_utf8_lossy(&output.stdout)
/// };
///
/// println!("{}", output);
/// # Ok::<_, Report>(())
/// ```
fn with_section<C, F>(self, section: F) -> Result<T>
where
C: Into<Section>,
F: FnOnce() -> C;
/// Add an error section to an error report, to be displayed after the primary error message
/// section.
///
/// # Examples
///
/// ```rust,should_panic
/// use color_eyre::{Report, Help};
/// use eyre::eyre;
/// use thiserror::Error;
///
/// #[derive(Debug, Error)]
/// #[error("{0}")]
/// struct StrError(&'static str);
///
/// Err(eyre!("command failed"))
/// .error(StrError("got one error"))
/// .error(StrError("got a second error"))?;
/// # Ok::<_, Report>(())
/// ```
fn error<E>(self, error: E) -> Result<T>
where
E: std::error::Error + Send + Sync + 'static;
/// Add an error section to an error report, to be displayed after the primary error message
/// section. The closure to create the Section is lazily evaluated only in the case of an error.
///
/// # Examples
///
/// ```rust,should_panic
/// use color_eyre::{Report, Help};
/// use eyre::eyre;
/// use thiserror::Error;
///
/// #[derive(Debug, Error)]
/// #[error("{0}")]
/// struct StringError(String);
///
/// Err(eyre!("command failed"))
/// .with_error(|| StringError("got one error".into()))
/// .with_error(|| StringError("got a second error".into()))?;
/// # Ok::<_, Report>(())
/// ```
fn with_error<E, F>(self, error: F) -> Result<T>
where
F: FnOnce() -> E,
E: std::error::Error + Send + Sync + 'static;
/// Add a Note to an error report, to be displayed after the chain of errors.
///
/// # Examples
///
/// ```rust
/// # use std::{error::Error, fmt::{self, Display}};
/// # use color_eyre::Result;
/// # #[derive(Debug)]
/// # struct FakeErr;
/// # impl Display for FakeErr {
/// # fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
/// # write!(f, "FakeErr")
/// # }
/// # }
/// # impl std::error::Error for FakeErr {}
/// # fn main() -> Result<()> {
/// # fn fallible_fn() -> Result<(), FakeErr> {
/// # Ok(())
/// # }
/// use color_eyre::Help as _;
///
/// fallible_fn().note("This might have failed due to ...")?;
/// # Ok(())
/// # }
/// ```
fn note<C>(self, context: C) -> Result<T>
where
C: Display + Send + Sync + 'static;
/// Add a Note to an error report, to be displayed after the chain of errors. The closure to
/// create the Note is lazily evaluated only in the case of an error.
///
/// # Examples
///
/// ```rust
/// # use std::{error::Error, fmt::{self, Display}};
/// # use color_eyre::Result;
/// # #[derive(Debug)]
/// # struct FakeErr;
/// # impl Display for FakeErr {
/// # fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
/// # write!(f, "FakeErr")
/// # }
/// # }
/// # impl std::error::Error for FakeErr {}
/// # fn main() -> Result<()> {
/// # fn fallible_fn() -> Result<(), FakeErr> {
/// # Ok(())
/// # }
/// use color_eyre::Help as _;
///
/// fallible_fn().with_note(|| {
/// format!("This might have failed due to ... It has failed {} times", 100)
/// })?;
/// # Ok(())
/// # }
/// ```
fn with_note<C, F>(self, f: F) -> Result<T>
where
C: Display + Send + Sync + 'static,
F: FnOnce() -> C;
/// Add a Warning to an error report, to be displayed after the chain of errors.
fn warning<C>(self, context: C) -> Result<T>
where
C: Display + Send + Sync + 'static;
/// Add a Warning to an error report, to be displayed after the chain of errors. The closure to
/// create the Warning is lazily evaluated only in the case of an error.
fn with_warning<C, F>(self, f: F) -> Result<T>
where
C: Display + Send + Sync + 'static,
F: FnOnce() -> C;
/// Add a Suggestion to an error report, to be displayed after the chain of errors.
fn suggestion<C>(self, context: C) -> Result<T>
where
C: Display + Send + Sync + 'static;
/// Add a Suggestion to an error report, to be displayed after the chain of errors. The closure
/// to create the Suggestion is lazily evaluated only in the case of an error.
fn with_suggestion<C, F>(self, f: F) -> Result<T>
where
C: Display + Send + Sync + 'static,
F: FnOnce() -> C;
}
impl<T, E> Help<T> for std::result::Result<T, E>
where
E: Into<Report>,
{
fn note<C>(self, context: C) -> Result<T>
where
C: Display + Send + Sync + 'static,
{
self.map_err(|e| {
let mut e = e.into();
e.context_mut().sections.push(
Section::from(HelpInfo::Note(Box::new(context)))
.order(section::Order::AfterBackTrace),
);
e
})
}
fn with_note<C, F>(self, context: F) -> Result<T>
where
C: Display + Send + Sync + 'static,
F: FnOnce() -> C,
{
self.map_err(|e| {
let mut e = e.into();
e.context_mut().sections.push(
Section::from(HelpInfo::Note(Box::new(context())))
.order(section::Order::AfterBackTrace),
);
e
})
}
fn warning<C>(self, context: C) -> Result<T>
where
C: Display + Send + Sync + 'static,
{
self.map_err(|e| {
let mut e = e.into();
e.context_mut().sections.push(
Section::from(HelpInfo::Warning(Box::new(context)))
.order(section::Order::AfterBackTrace),
);
e
})
}
fn with_warning<C, F>(self, context: F) -> Result<T>
where
C: Display + Send + Sync + 'static,
F: FnOnce() -> C,
{
self.map_err(|e| {
let mut e = e.into();
e.context_mut().sections.push(
Section::from(HelpInfo::Warning(Box::new(context())))
.order(section::Order::AfterBackTrace),
);
e
})
}
fn suggestion<C>(self, context: C) -> Result<T>
where
C: Display + Send + Sync + 'static,
{
self.map_err(|e| {
let mut e = e.into();
e.context_mut().sections.push(
Section::from(HelpInfo::Suggestion(Box::new(context)))
.order(section::Order::AfterBackTrace),
);
e
})
}
fn with_suggestion<C, F>(self, context: F) -> Result<T>
where
C: Display + Send + Sync + 'static,
F: FnOnce() -> C,
{
self.map_err(|e| {
let mut e = e.into();
e.context_mut().sections.push(
Section::from(HelpInfo::Suggestion(Box::new(context())))
.order(section::Order::AfterBackTrace),
);
e
})
}
fn with_section<C, F>(self, section: F) -> Result<T>
where
C: Into<Section>,
F: FnOnce() -> C,
{
self.map_err(|e| {
let mut e = e.into();
let section = section().into();
if !matches!(section.order, section::Order::SkipEntirely) {
e.context_mut().sections.push(section);
}
e
})
}
fn section<C>(self, section: C) -> Result<T>
where
C: Into<Section>,
{
self.map_err(|e| {
let mut e = e.into();
let section = section.into();
if !matches!(section.order, section::Order::SkipEntirely) {
e.context_mut().sections.push(section);
}
e
})
}
fn error<E2>(self, error: E2) -> Result<T>
where
E2: std::error::Error + Send + Sync + 'static,
{
self.map_err(|e| {
let mut e = e.into();
let section = Section {
inner: section::SectionKind::Error(Box::new(error)),
order: section::Order::AfterErrMsgs,
};
e.context_mut().sections.push(section);
e
})
}
fn with_error<E2, F>(self, error: F) -> Result<T>
where
F: FnOnce() -> E2,
E2: std::error::Error + Send + Sync + 'static,
{
self.map_err(|e| {
let mut e = e.into();
let section = Section {
inner: section::SectionKind::Error(Box::new(error())),
order: section::Order::AfterErrMsgs,
};
e.context_mut().sections.push(section);
e
})
}
}
pub(crate) enum HelpInfo {
Note(Box<dyn Display + Send + Sync + 'static>),
Warning(Box<dyn Display + Send + Sync + 'static>),
Suggestion(Box<dyn Display + Send + Sync + 'static>),
}
impl Display for HelpInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Note(context) => write!(f, "{}: {}", Cyan.paint("Note"), context),
Self::Warning(context) => write!(f, "{}: {}", Yellow.paint("Warning"), context),
Self::Suggestion(context) => write!(f, "{}: {}", Cyan.paint("Suggestion"), context),
}
}
}
impl fmt::Debug for HelpInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Note(context) => f
.debug_tuple("Note")
.field(&format_args!("{}", context))
.finish(),
Self::Warning(context) => f
.debug_tuple("Warning")
.field(&format_args!("{}", context))
.finish(),
Self::Suggestion(context) => f
.debug_tuple("Suggestion")
.field(&format_args!("{}", context))
.finish(),
}
}
}
pub(crate) mod private {
use crate::Report;
pub trait Sealed {}
impl<T, E> Sealed for std::result::Result<T, E> where E: Into<Report> {}
}

248
src/section/mod.rs Normal file
View File

@ -0,0 +1,248 @@
//! Helpers for adding custom sections to error reports
use ansi_term::Color::*;
use indenter::indented;
use std::fmt::{self, Display, Write};
pub mod help;
#[non_exhaustive]
#[derive(Debug)]
pub(crate) enum Order {
AfterErrMsgs,
BeforeSpanTrace,
AfterBackTrace,
SkipEntirely,
}
/// A custom section for an error report.
///
/// # Details
///
/// Sections consist of two parts, a header: and an optional body. The header can contain any
/// number of lines and has no indentation applied to it by default. The body can contain any
/// number of lines and is always written after the header with indentation inserted before
/// every line.
///
/// # Construction
///
/// Sections are meant to be constructed via `Into<Section>`, which is implemented for all types
/// that implement `Display`. The constructed `Section` then takes ownership of the `Display` type
/// and boxes it internally for use later when printing the report.
///
/// # Examples
///
/// ```rust
/// use color_eyre::{SectionExt, Help, Report};
/// use eyre::eyre;
/// use std::process::Command;
/// use tracing::instrument;
///
/// trait Output {
/// fn output2(&mut self) -> Result<String, Report>;
/// }
///
/// impl Output for Command {
/// #[instrument]
/// fn output2(&mut self) -> Result<String, Report> {
/// let output = self.output()?;
///
/// let stdout = String::from_utf8_lossy(&output.stdout);
///
/// if !output.status.success() {
/// let stderr = String::from_utf8_lossy(&output.stderr);
/// Err(eyre!("cmd exited with non-zero status code"))
/// .with_section(move || {
/// "Stdout:"
/// .skip_if(|| stdout.is_empty())
/// .body(stdout.trim().to_string())
/// })
/// .with_section(move || {
/// "Stderr:"
/// .skip_if(|| stderr.is_empty())
/// .body(stderr.trim().to_string())
/// })
/// } else {
/// Ok(stdout.into())
/// }
/// }
/// }
/// ```
pub struct Section {
pub(crate) inner: SectionKind,
pub(crate) order: Order,
}
pub(crate) enum SectionKind {
Header(Box<dyn Display + Send + Sync + 'static>),
WithBody(
Box<dyn Display + Send + Sync + 'static>,
Box<dyn Display + Send + Sync + 'static>,
),
Error(Box<dyn std::error::Error + Send + Sync + 'static>),
}
/// Extension trait for customizing the content of a `Section`
pub trait SectionExt {
/// Add a body to a `Section`
///
/// Bodies are always indented to the same level as error messages and spans.
///
/// # Examples
///
/// ```rust
/// use color_eyre::{Help, SectionExt, Report};
/// use eyre::eyre;
///
/// let all_in_header = "header\n body\n body";
/// let report = Err::<(), Report>(eyre!("an error occurred"))
/// .section(all_in_header)
/// .unwrap_err();
///
/// let just_header = "header";
/// let just_body = "body\nbody";
/// let report2 = Err::<(), Report>(eyre!("an error occurred"))
/// .section(just_header.body(just_body))
/// .unwrap_err();
///
/// assert_eq!(format!("{:?}", report), format!("{:?}", report2))
/// ```
fn body<C>(self, body: C) -> Section
where
C: Display + Send + Sync + 'static;
/// Skip a section based on some condition. For example, skip a section if the body is empty.
///
/// The skipped section is not stored in the report and is instead immediately dropped.
///
/// # Examples
///
/// ```rust
/// use eyre::eyre;
/// use color_eyre::{SectionExt, Report, Help};
///
/// fn add_body(report: Report, body: String) -> Result<(), Report> {
/// Err(report)
/// .with_section(|| "ExtraInfo:".skip_if(|| body.is_empty()).body(body))
/// }
///
/// let report = eyre!("an error occurred");
/// let before = format!("{:?}", report);
/// let body = String::new();
/// let report = add_body(report, body).unwrap_err();
/// let after = format!("{:?}", report);
/// assert_eq!(before, after);
///
/// let report = eyre!("an error occurred");
/// let before = format!("{:?}", report);
/// let body = String::from("Some actual text here");
/// let report = add_body(report, body).unwrap_err();
/// let after = format!("{:?}", report);
/// assert_ne!(before, after);
/// ```
fn skip_if<F>(self, condition: F) -> Section
where
F: FnOnce() -> bool;
}
impl Section {
pub(crate) fn order(mut self, order: Order) -> Self {
self.order = order;
self
}
}
impl<T> SectionExt for T
where
Section: From<T>,
{
fn body<C>(self, body: C) -> Section
where
C: Display + Send + Sync + 'static,
{
let section = Section::from(self);
let header = match section.inner {
SectionKind::Header(header) => header,
SectionKind::WithBody(header, _body) => header,
SectionKind::Error(_) => unreachable!("bodies cannot be added to Error sections"),
};
let inner = SectionKind::WithBody(header, Box::new(body));
Section {
inner,
order: section.order,
}
}
fn skip_if<F>(self, condition: F) -> Section
where
F: FnOnce() -> bool,
{
let mut section = Section::from(self);
section.order = if condition() {
Order::SkipEntirely
} else {
section.order
};
section
}
}
impl<T> From<T> for Section
where
T: Display + Send + Sync + 'static,
{
fn from(header: T) -> Self {
let inner = SectionKind::Header(Box::new(header));
Self {
inner,
order: Order::BeforeSpanTrace,
}
}
}
impl fmt::Debug for Section {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.inner)
}
}
impl fmt::Display for SectionKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SectionKind::Header(header) => write!(f, "{}", header)?,
SectionKind::WithBody(header, body) => {
write!(f, "{}", header)?;
writeln!(f)?;
write!(
indenter::indented(f)
.with_format(indenter::Format::Uniform { indentation: " " }),
"{}",
body
)?;
}
SectionKind::Error(error) => {
// a lot here
let errors = std::iter::successors(
Some(error.as_ref() as &(dyn std::error::Error + 'static)),
|e| e.source(),
);
write!(f, "Error:")?;
let mut buf = String::new();
for (n, error) in errors.enumerate() {
writeln!(f)?;
buf.clear();
write!(&mut buf, "{}", error).unwrap();
write!(indented(f).ind(n), "{}", Red.paint(&buf))?;
}
}
}
Ok(())
}
}