diff --git a/Cargo.toml b/Cargo.toml index ce76bc6..8943d75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.3.2" authors = ["Jane Lusby "] edition = "2018" license = "MIT OR Apache-2.0" -description = "A custom context for the `eyre` crate for colorful error reports, suggestions, and `tracing-error` support." +description = "An error report handler for panics and eyre::Reports for colorful, consistent, and well formatted error reports for all kinds of errors." repository = "https://github.com/yaahc/color-eyre" documentation = "https://docs.rs/color-eyre" readme = "README.md" @@ -16,7 +16,7 @@ default = ["capture-spantrace"] capture-spantrace = ["tracing-error", "color-spantrace"] [dependencies] -eyre = "0.4.2" +eyre = "0.4.3" tracing-error = { version = "0.1.2", optional = true } color-backtrace = "0.4.0" backtrace = "0.3.48" diff --git a/README.md b/README.md index f2eabff..969b12c 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ [docs-badge]: https://img.shields.io/badge/docs-latest-blue.svg [docs-url]: https://docs.rs/color-eyre -A custom context for the [`eyre`] crate for colorful error reports with suggestions, custom -sections, [`tracing-error`] support, and backtraces on stable. +An error report handler for panics and the [`eyre`] crate for colorful, consistent, and well +formatted error reports for all kinds of errors. ## TLDR @@ -82,53 +82,7 @@ opt-level = 3 ### 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 -use color_eyre::{Help, Report}; -use eyre::WrapErr; -use tracing::{info, instrument}; - -#[instrument] -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 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<(), Report> { - info!("Reading file"); - Ok(std::fs::read_to_string(path).map(drop)?) -} - -#[instrument] -fn read_config() -> Result<(), Report> { - read_file("fake_file") - .wrap_err("Unable to read config") - .suggestion("try using a file that exists next time") -} -``` +and `Backtrace`, minimal, short, and full. Take the below screenshots of the output produced by [`examples/usage.rs`]: --- @@ -154,9 +108,10 @@ error originated from, assuming it can find them on the disk. ### 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 +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 @@ -179,16 +134,8 @@ impl Output for Command { 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()) - }) + .with_section(move || stdout.trim().to_string().header("Stdout:")) + .with_section(move || stderr.trim().to_string().header("Stderr:")) } else { Ok(stdout.into()) } @@ -198,26 +145,25 @@ impl Output for Command { --- -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. +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`. -Running `cargo run --example custom_section` shows us how these sections are included in the -output: +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. +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 +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 @@ -228,41 +174,43 @@ For an example of how to aggregate errors check out [`examples/multiple_errors.r ### 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. +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 -This crate works by defining a `Context` type which implements [`eyre::EyreContext`] -and a pair of type aliases for setting this context type as the parameter of -[`eyre::Report`]. +This crate works by defining a `Handler` type which implements +[`eyre::EyreHandler`] and a pair of type aliases for setting this handler +type as the parameter of [`eyre::Report`]. ```rust -use color_eyre::Context; +use color_eyre::Handler; -pub type Report = eyre::Report; +pub type Report = eyre::Report; pub type Result = core::result::Result; ``` -Please refer to the [`Context`] type's docs for more details about its feature set. +Please refer to the [`Handler`] type's docs for more details about its feature set. [`eyre`]: https://docs.rs/eyre [`tracing-error`]: https://docs.rs/tracing-error [`color-backtrace`]: https://docs.rs/color-backtrace -[`eyre::EyreContext`]: https://docs.rs/eyre/*/eyre/trait.EyreContext.html +[`eyre::EyreHandler`]: https://docs.rs/eyre/*/eyre/trait.EyreHandler.html [`backtrace::Backtrace`]: https://docs.rs/backtrace/*/backtrace/struct.Backtrace.html [`tracing_error::SpanTrace`]: https://docs.rs/tracing-error/*/tracing_error/struct.SpanTrace.html [`color-spantrace`]: https://github.com/yaahc/color-spantrace [`Help`]: https://docs.rs/color-eyre/*/color_eyre/trait.Help.html [`eyre::Report`]: https://docs.rs/eyre/*/eyre/struct.Report.html [`eyre::Result`]: https://docs.rs/eyre/*/eyre/type.Result.html -[`Context`]: https://docs.rs/color-eyre/*/color_eyre/struct.Context.html +[`Handler`]: https://docs.rs/color-eyre/*/color_eyre/struct.Handler.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 diff --git a/examples/custom_section.rs b/examples/custom_section.rs index 0751416..9f11b35 100644 --- a/examples/custom_section.rs +++ b/examples/custom_section.rs @@ -17,16 +17,8 @@ impl Output for Command { 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()) - }) + .with_section(move || stdout.trim().to_string().header("Stdout:")) + .with_section(move || stderr.trim().to_string().header("Stderr:")) } else { Ok(stdout.into()) } @@ -61,7 +53,7 @@ fn install_tracing() { #[instrument] fn read_file(path: &str) -> Result { - Command::new("cat").arg("fake_file").output2() + Command::new("cat").arg(path).output2() } #[instrument] diff --git a/examples/debug_perf.rs b/examples/debug_perf.rs index cb64141..5c8fd8a 100644 --- a/examples/debug_perf.rs +++ b/examples/debug_perf.rs @@ -26,7 +26,7 @@ fn time_report_inner() { .suggestion("try using a file that exists next time") .unwrap_err(); - let _ = println!("Error: {:?}", report); + println!("Error: {:?}", report); drop(report); let end = std::time::Instant::now(); diff --git a/src/lib.rs b/src/lib.rs index 5e18c73..6c994c5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ -//! A custom context for the [`eyre`] crate for colorful error reports with suggestions, custom -//! sections, [`tracing-error`] support, and backtraces on stable. +//! An error report handler for panics and the [`eyre`] crate for colorful, consistent, and well +//! formatted error reports for all kinds of errors. //! //! ## TLDR //! @@ -69,53 +69,7 @@ //! ### 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 -//! use color_eyre::{Help, Report}; -//! use eyre::WrapErr; -//! use tracing::{info, instrument}; -//! -//! #[instrument] -//! 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 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<(), Report> { -//! info!("Reading file"); -//! Ok(std::fs::read_to_string(path).map(drop)?) -//! } -//! -//! #[instrument] -//! fn read_config() -> Result<(), Report> { -//! read_file("fake_file") -//! .wrap_err("Unable to read config") -//! .suggestion("try using a file that exists next time") -//! } -//! ``` +//! and `Backtrace`, minimal, short, and full. Take the below screenshots of the output produced by [`examples/usage.rs`]: //! //! --- //! @@ -141,9 +95,10 @@ //! //! ### 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 +//! 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 @@ -166,16 +121,8 @@ //! 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()) -//! }) +//! .with_section(move || stdout.trim().to_string().header("Stdout:")) +//! .with_section(move || stderr.trim().to_string().header("Stderr:")) //! } else { //! Ok(stdout.into()) //! } @@ -185,26 +132,25 @@ //! //! --- //! -//! 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. +//! 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`. //! -//! Running `cargo run --example custom_section` shows us how these sections are included in the -//! output: +//! 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. +//! 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 +//! 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 @@ -215,41 +161,43 @@ //! //! ### 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. +//! 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 //! -//! This crate works by defining a `Context` type which implements [`eyre::EyreContext`] -//! and a pair of type aliases for setting this context type as the parameter of -//! [`eyre::Report`]. +//! This crate works by defining a `Handler` type which implements +//! [`eyre::EyreHandler`] and a pair of type aliases for setting this handler +//! type as the parameter of [`eyre::Report`]. //! //! ```rust -//! use color_eyre::Context; +//! use color_eyre::Handler; //! -//! pub type Report = eyre::Report; +//! pub type Report = eyre::Report; //! pub type Result = core::result::Result; //! ``` //! -//! Please refer to the [`Context`] type's docs for more details about its feature set. +//! Please refer to the [`Handler`] type's docs for more details about its feature set. //! //! [`eyre`]: https://docs.rs/eyre //! [`tracing-error`]: https://docs.rs/tracing-error //! [`color-backtrace`]: https://docs.rs/color-backtrace -//! [`eyre::EyreContext`]: https://docs.rs/eyre/*/eyre/trait.EyreContext.html +//! [`eyre::EyreHandler`]: https://docs.rs/eyre/*/eyre/trait.EyreHandler.html //! [`backtrace::Backtrace`]: https://docs.rs/backtrace/*/backtrace/struct.Backtrace.html //! [`tracing_error::SpanTrace`]: https://docs.rs/tracing-error/*/tracing_error/struct.SpanTrace.html //! [`color-spantrace`]: https://github.com/yaahc/color-spantrace //! [`Help`]: https://docs.rs/color-eyre/*/color_eyre/trait.Help.html //! [`eyre::Report`]: https://docs.rs/eyre/*/eyre/struct.Report.html //! [`eyre::Result`]: https://docs.rs/eyre/*/eyre/type.Result.html -//! [`Context`]: https://docs.rs/color-eyre/*/color_eyre/struct.Context.html +//! [`Handler`]: https://docs.rs/color-eyre/*/color_eyre/struct.Handler.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 @@ -279,13 +227,14 @@ unused_parens, while_true )] +use crate::writers::HeaderWriter; use ansi_term::Color::*; use backtrace::Backtrace; pub use color_backtrace::BacktracePrinter; use eyre::*; use indenter::{indented, Format}; use once_cell::sync::OnceCell; -use section::Order; +use section::help::HelpInfo; pub use section::{help::Help, Section, SectionExt}; #[cfg(feature = "capture-spantrace")] use std::error::Error; @@ -296,12 +245,15 @@ use std::{ }; #[cfg(feature = "capture-spantrace")] use tracing_error::{ExtractSpanTrace, SpanTrace, SpanTraceStatus}; +#[doc(hidden)] +pub use Handler as Context; pub mod section; +mod writers; static CONFIG: OnceCell = OnceCell::new(); -/// A custom context type for [`eyre::Report`] which provides colorful error +/// A custom handler type for [`eyre::Report`] which provides colorful error /// reports and [`tracing-error`] support. /// /// This type is not intended to be used directly, prefer using it via the @@ -312,17 +264,19 @@ static CONFIG: OnceCell = OnceCell::new(); /// [`color_eyre::Report`]: type.Report.html /// [`color_eyre::Result`]: type.Result.html #[derive(Debug)] -pub struct Context { +pub struct Handler { backtrace: Option, #[cfg(feature = "capture-spantrace")] span_trace: Option, - sections: Vec
, + sections: Vec, } #[derive(Debug)] struct InstallError; +#[cfg(feature = "capture-spantrace")] +struct FormattedSpanTrace<'a>(&'a SpanTrace); -impl Context { +impl Handler { /// Return a reference to the captured `Backtrace` type /// /// # Examples @@ -336,7 +290,7 @@ impl Context { /// std::env::set_var("RUST_BACKTRACE", "1"); /// /// let report: Report = eyre!("an error occurred"); - /// assert!(report.context().backtrace().is_some()); + /// assert!(report.handler().backtrace().is_some()); /// ``` /// /// Alternatively, if you don't want backtraces to be printed on panic, you can use @@ -349,7 +303,7 @@ impl Context { /// std::env::set_var("RUST_LIB_BACKTRACE", "1"); /// /// let report: Report = eyre!("an error occurred"); - /// assert!(report.context().backtrace().is_some()); + /// assert!(report.handler().backtrace().is_some()); /// ``` /// /// And if you don't want backtraces to be captured but you still want panics to print @@ -363,7 +317,7 @@ impl Context { /// std::env::set_var("RUST_LIB_BACKTRACE", "0"); /// /// let report: Report = eyre!("an error occurred"); - /// assert!(report.context().backtrace().is_none()); + /// assert!(report.handler().backtrace().is_none()); /// ``` /// pub fn backtrace(&self) -> Option<&Backtrace> { @@ -381,7 +335,7 @@ impl Context { /// use eyre::eyre; /// /// let report: Report = eyre!("an error occurred"); - /// assert!(report.context().span_trace().is_some()); + /// assert!(report.handler().span_trace().is_some()); /// ``` /// /// However, `SpanTrace` is not captured if one of the source errors already captured a @@ -409,7 +363,7 @@ impl Context { /// let error: TracedError = error.in_current_span(); /// /// let report: Report = error.into(); - /// assert!(report.context().span_trace().is_none()); + /// assert!(report.handler().span_trace().is_none()); /// ``` /// /// [`tracing_error::TracedError`]: https://docs.rs/tracing-error/0.1.2/tracing_error/struct.TracedError.html @@ -420,7 +374,7 @@ impl Context { } } -impl EyreContext for Context { +impl EyreHandler for Handler { #[allow(unused_variables)] fn default(error: &(dyn std::error::Error + 'static)) -> Self { let backtrace = if backtrace_enabled() { @@ -463,26 +417,32 @@ impl EyreContext for Context { let mut buf = String::new(); for (n, error) in errors { - writeln!(f)?; buf.clear(); write!(&mut buf, "{}", error).unwrap(); + writeln!(f)?; write!(indented(f).ind(n), "{}", Red.paint(&buf))?; } + let separated = &mut HeaderWriter { + inner: &mut *f, + header: &"\n\n", + started: false, + }; + for section in self .sections .iter() - .filter(|s| matches!(s.order, Order::AfterErrMsgs)) + .filter(|s| matches!(s, HelpInfo::Error(_))) { - write!(f, "\n\n{:?}", section)?; + write!(separated.ready(), "{}", section)?; } for section in self .sections .iter() - .filter(|s| matches!(s.order, Order::BeforeSpanTrace)) + .filter(|s| matches!(s, HelpInfo::Custom(_))) { - write!(f, "\n\n{:?}", section)?; + write!(separated.ready(), "{}", section)?; } #[cfg(feature = "capture-spantrace")] @@ -493,33 +453,23 @@ impl EyreContext for Context { .or_else(|| get_deepest_spantrace(error)) .expect("SpanTrace capture failed"); - match span_trace.status() { - SpanTraceStatus::CAPTURED => { - write!(f, "\n\n")?; - write!(indented(f).with_format(Format::Uniform { indentation: " " }), "{}", color_spantrace::colorize(span_trace))? - }, - SpanTraceStatus::UNSUPPORTED => write!(f, "\n\nWarning: SpanTrace capture is Unsupported.\nEnsure that you've setup an error layer and the versions match")?, - _ => (), - } + write!(&mut separated.ready(), "{}", FormattedSpanTrace(span_trace))?; } if let Some(backtrace) = self.backtrace.as_ref() { - write!(f, "\n\n")?; - - let bt_str = CONFIG - .get_or_init(default_printer) + let bt_str = installed_printer() .format_trace_to_string(&backtrace) .unwrap(); write!( - indented(f).with_format(Format::Uniform { indentation: " " }), + indented(&mut separated.ready()).with_format(Format::Uniform { indentation: " " }), "{}", bt_str )?; } else if self .sections .iter() - .any(|s| matches!(s.order, Order::AfterBackTrace)) + .any(|s| !matches!(s, HelpInfo::Custom(_) | HelpInfo::Error(_))) { writeln!(f)?; } @@ -527,9 +477,26 @@ impl EyreContext for Context { for section in self .sections .iter() - .filter(|s| matches!(s.order, Order::AfterBackTrace)) + .filter(|s| !matches!(s, HelpInfo::Custom(_) | HelpInfo::Error(_))) { - write!(f, "\n{:?}", section)?; + write!(f, "\n{}", section)?; + } + + Ok(()) + } +} + +#[cfg(feature = "capture-spantrace")] +impl fmt::Display for FormattedSpanTrace<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use std::fmt::Write; + + match self.0.status() { + SpanTraceStatus::CAPTURED => { + write!(indented(f).with_format(Format::Uniform { indentation: " " }), "{}", color_spantrace::colorize(self.0))?; + }, + SpanTraceStatus::UNSUPPORTED => write!(f, "Warning: SpanTrace capture is Unsupported.\nEnsure that you've setup an error layer and the versions match")?, + _ => (), } Ok(()) @@ -573,7 +540,7 @@ fn get_deepest_spantrace<'a>(error: &'a (dyn Error + 'static)) -> Option<&'a Spa .next() } -/// Override the global BacktracePrinter used by `color_eyre::Context` when printing captured +/// Override the global BacktracePrinter used by `color_eyre::Handler` when printing captured /// backtraces. /// /// # Examples @@ -606,7 +573,16 @@ fn get_deepest_spantrace<'a>(error: &'a (dyn Error + 'static)) -> Option<&'a Spa /// ``` pub fn install(printer: BacktracePrinter) -> Result<(), impl std::error::Error> { let printer = add_eyre_filters(printer); - CONFIG.set(printer).map_err(|_| InstallError) + + if CONFIG.set(printer).is_err() { + return Err(InstallError); + } + + Ok(()) +} + +fn installed_printer() -> &'static color_backtrace::BacktracePrinter { + CONFIG.get_or_init(default_printer) } fn default_printer() -> BacktracePrinter { @@ -616,7 +592,7 @@ fn default_printer() -> BacktracePrinter { fn add_eyre_filters(printer: BacktracePrinter) -> BacktracePrinter { printer.add_frame_filter(Box::new(|frames| { let filters = &[ - "::default", + "::default", "eyre::", "color_eyre::", ]; @@ -635,7 +611,7 @@ fn add_eyre_filters(printer: BacktracePrinter) -> BacktracePrinter { })) } -/// A type alias for `eyre::Report` +/// A type alias for `eyre::Report` /// /// # Example /// @@ -648,7 +624,7 @@ fn add_eyre_filters(printer: BacktracePrinter) -> BacktracePrinter { /// # Ok(Config) /// } /// ``` -pub type Report = eyre::Report; +pub type Report = eyre::Report; /// A type alias for `Result` /// diff --git a/src/section/help.rs b/src/section/help.rs index f48c094..8915392 100644 --- a/src/section/help.rs +++ b/src/section/help.rs @@ -1,6 +1,8 @@ //! Provides an extension trait for attaching `Section`s to error reports. -use crate::{section, Report, Result, Section}; +use crate::{Report, Result}; use ansi_term::Color::*; +use indenter::indented; +use std::fmt::Write; use std::fmt::{self, Display}; /// A helper trait for attaching help text to errors to be displayed after the chain of errors @@ -34,9 +36,9 @@ pub trait Help: private::Sealed { /// .section("Please report bugs to https://real.url/bugs")?; /// # Ok::<_, Report>(()) /// ``` - fn section(self, section: C) -> Result + fn section(self, section: D) -> Result where - C: Into
; + D: Display + Send + Sync + 'static; /// 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. @@ -53,11 +55,7 @@ pub trait Help: private::Sealed { /// 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()) - /// })? + /// .with_section(move || stderr.trim().to_string().header("Stderr:"))? /// } else { /// String::from_utf8_lossy(&output.stdout) /// }; @@ -65,10 +63,10 @@ pub trait Help: private::Sealed { /// println!("{}", output); /// # Ok::<_, Report>(()) /// ``` - fn with_section(self, section: F) -> Result + fn with_section(self, section: F) -> Result where - C: Into
, - F: FnOnce() -> C; + D: Display + Send + Sync + 'static, + F: FnOnce() -> D; /// Add an error section to an error report, to be displayed after the primary error message /// section. @@ -142,9 +140,9 @@ pub trait Help: private::Sealed { /// # Ok(()) /// # } /// ``` - fn note(self, context: C) -> Result + fn note(self, note: D) -> Result where - C: Display + Send + Sync + 'static; + D: 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. @@ -174,156 +172,142 @@ pub trait Help: private::Sealed { /// # Ok(()) /// # } /// ``` - fn with_note(self, f: F) -> Result + fn with_note(self, f: F) -> Result where - C: Display + Send + Sync + 'static, - F: FnOnce() -> C; + D: Display + Send + Sync + 'static, + F: FnOnce() -> D; /// Add a Warning to an error report, to be displayed after the chain of errors. - fn warning(self, context: C) -> Result + fn warning(self, warning: D) -> Result where - C: Display + Send + Sync + 'static; + D: 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(self, f: F) -> Result + fn with_warning(self, f: F) -> Result where - C: Display + Send + Sync + 'static, - F: FnOnce() -> C; + D: Display + Send + Sync + 'static, + F: FnOnce() -> D; /// Add a Suggestion to an error report, to be displayed after the chain of errors. - fn suggestion(self, context: C) -> Result + fn suggestion(self, suggestion: D) -> Result where - C: Display + Send + Sync + 'static; + D: 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(self, f: F) -> Result + fn with_suggestion(self, f: F) -> Result where - C: Display + Send + Sync + 'static, - F: FnOnce() -> C; + D: Display + Send + Sync + 'static, + F: FnOnce() -> D; } impl Help for std::result::Result where E: Into, { - fn note(self, context: C) -> Result + fn note(self, note: D) -> Result where - C: Display + Send + Sync + 'static, + D: 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.handler_mut() + .sections + .push(HelpInfo::Note(Box::new(note))); e }) } - fn with_note(self, context: F) -> Result + fn with_note(self, note: F) -> Result where - C: Display + Send + Sync + 'static, - F: FnOnce() -> C, + D: Display + Send + Sync + 'static, + F: FnOnce() -> D, { 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.handler_mut() + .sections + .push(HelpInfo::Note(Box::new(note()))); e }) } - fn warning(self, context: C) -> Result + fn warning(self, warning: D) -> Result where - C: Display + Send + Sync + 'static, + D: 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.handler_mut() + .sections + .push(HelpInfo::Warning(Box::new(warning))); e }) } - fn with_warning(self, context: F) -> Result + fn with_warning(self, warning: F) -> Result where - C: Display + Send + Sync + 'static, - F: FnOnce() -> C, + D: Display + Send + Sync + 'static, + F: FnOnce() -> D, { 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.handler_mut() + .sections + .push(HelpInfo::Warning(Box::new(warning()))); e }) } - fn suggestion(self, context: C) -> Result + fn suggestion(self, suggestion: D) -> Result where - C: Display + Send + Sync + 'static, + D: 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.handler_mut() + .sections + .push(HelpInfo::Suggestion(Box::new(suggestion))); e }) } - fn with_suggestion(self, context: F) -> Result + fn with_suggestion(self, suggestion: F) -> Result where - C: Display + Send + Sync + 'static, - F: FnOnce() -> C, + D: Display + Send + Sync + 'static, + F: FnOnce() -> D, { 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.handler_mut() + .sections + .push(HelpInfo::Suggestion(Box::new(suggestion()))); e }) } - fn with_section(self, section: F) -> Result + fn with_section(self, section: F) -> Result where - C: Into
, - F: FnOnce() -> C, + D: Display + Send + Sync + 'static, + F: FnOnce() -> D, { 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); - } - + let section = Box::new(section()); + e.handler_mut().sections.push(HelpInfo::Custom(section)); e }) } - fn section(self, section: C) -> Result + fn section(self, section: D) -> Result where - C: Into
, + D: Display + Send + Sync + 'static, { 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); - } - + let section = Box::new(section); + e.handler_mut().sections.push(HelpInfo::Custom(section)); e }) } @@ -334,12 +318,9 @@ where { self.map_err(|e| { let mut e = e.into(); - let section = Section { - inner: section::SectionKind::Error(Box::new(error)), - order: section::Order::AfterErrMsgs, - }; + let error = error.into(); - e.context_mut().sections.push(section); + e.handler_mut().sections.push(HelpInfo::Error(error)); e }) } @@ -351,18 +332,17 @@ where { self.map_err(|e| { let mut e = e.into(); - let section = Section { - inner: section::SectionKind::Error(Box::new(error())), - order: section::Order::AfterErrMsgs, - }; + let error = error().into(); - e.context_mut().sections.push(section); + e.handler_mut().sections.push(HelpInfo::Error(error)); e }) } } pub(crate) enum HelpInfo { + Error(Box), + Custom(Box), Note(Box), Warning(Box), Suggestion(Box), @@ -371,9 +351,28 @@ pub(crate) enum HelpInfo { 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), + HelpInfo::Note(n) => write!(f, "{}: {}", Cyan.paint("Note"), n), + HelpInfo::Warning(w) => write!(f, "{}: {}", Yellow.paint("Warning"), w), + HelpInfo::Suggestion(s) => write!(f, "{}: {}", Cyan.paint("Suggestion"), s), + HelpInfo::Custom(c) => write!(f, "{}", c), + HelpInfo::Error(e) => { + // a lot here + let errors = std::iter::successors( + Some(e.as_ref() as &(dyn std::error::Error + 'static)), + |e| e.source(), + ); + + write!(f, "Error:")?; + let mut buf = String::new(); + for (n, e) in errors.enumerate() { + writeln!(f)?; + buf.clear(); + write!(&mut buf, "{}", e).unwrap(); + write!(indented(f).ind(n), "{}", Red.paint(&buf))?; + } + + Ok(()) + } } } } @@ -381,18 +380,23 @@ impl Display for HelpInfo { impl fmt::Debug for HelpInfo { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Note(context) => f + HelpInfo::Note(note) => f .debug_tuple("Note") - .field(&format_args!("{}", context)) + .field(&format_args!("{}", note)) .finish(), - Self::Warning(context) => f + HelpInfo::Warning(warning) => f .debug_tuple("Warning") - .field(&format_args!("{}", context)) + .field(&format_args!("{}", warning)) .finish(), - Self::Suggestion(context) => f + HelpInfo::Suggestion(suggestion) => f .debug_tuple("Suggestion") - .field(&format_args!("{}", context)) + .field(&format_args!("{}", suggestion)) .finish(), + HelpInfo::Custom(custom) => f + .debug_tuple("CustomSection") + .field(&format_args!("{}", custom)) + .finish(), + HelpInfo::Error(error) => f.debug_tuple("Error").field(error).finish(), } } } diff --git a/src/section/mod.rs b/src/section/mod.rs index b231e49..58a34fd 100644 --- a/src/section/mod.rs +++ b/src/section/mod.rs @@ -1,33 +1,18 @@ //! 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. +/// An indenteted section with a header 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
`, 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. +/// This helper provides two functions to help with constructing nicely formatted +/// error reports. First, it handles indentation of every line of the body for +/// you, and makes sure it is consistent with the rest of color-eyre's output. +/// Second, it omits outputting the header if the body itself is empty, +/// preventing unnecessary pollution of the report for sections with dynamic +/// content. /// /// # Examples /// @@ -51,41 +36,27 @@ pub(crate) enum Order { /// 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()) -/// }) +/// .with_section(move || stdout.trim().to_string().header("Stdout:")) +/// .with_section(move || stderr.trim().to_string().header("Stderr:")) /// } else { /// Ok(stdout.into()) /// } /// } /// } /// ``` -pub struct Section { - pub(crate) inner: SectionKind, - pub(crate) order: Order, +#[allow(missing_debug_implementations)] +pub struct Section { + header: H, + body: B, } -pub(crate) enum SectionKind { - Header(Box), - WithBody( - Box, - Box, - ), - Error(Box), -} - -/// Extension trait for customizing the content of a `Section` -pub trait SectionExt { - /// Add a body to a `Section` +/// Extension trait for constructing sections with commonly used formats +pub trait SectionExt: Sized { + /// Add a header to a `Section` and indent the body /// /// Bodies are always indented to the same level as error messages and spans. + /// The header is not printed if the display impl of the body produces no + /// output. /// /// # Examples /// @@ -101,147 +72,52 @@ pub trait SectionExt { /// let just_header = "header"; /// let just_body = "body\nbody"; /// let report2 = Err::<(), Report>(eyre!("an error occurred")) - /// .section(just_header.body(just_body)) + /// .section(just_body.header(just_header)) /// .unwrap_err(); /// /// assert_eq!(format!("{:?}", report), format!("{:?}", report2)) /// ``` - fn body(self, body: C) -> Section + fn header(self, header: 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(self, condition: F) -> Section - where - F: FnOnce() -> bool; -} - -impl Section { - pub(crate) fn order(mut self, order: Order) -> Self { - self.order = order; - self - } } impl SectionExt for T where - Section: From, + T: Display + Send + Sync + 'static, { - fn body(self, body: C) -> Section + fn header(self, header: 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(self, condition: F) -> Section - where - F: FnOnce() -> bool, - { - let mut section = Section::from(self); - - section.order = if condition() { - Order::SkipEntirely - } else { - section.order - }; - - section + Section { body: self, header } } } -impl From for Section +impl fmt::Display for Section where - T: Display + Send + Sync + 'static, + H: Display + Send + Sync + 'static, + B: 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) - } -} + let mut headered = crate::writers::HeaderWriter { + inner: f, + header: &self.header, + started: false, + }; -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(), - ); + let mut headered = crate::writers::HeaderWriter { + inner: headered.ready(), + header: &"\n", + started: false, + }; - 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))?; - } - } - } + let mut headered = headered.ready(); + + let mut indented = indenter::indented(&mut headered) + .with_format(indenter::Format::Uniform { indentation: " " }); + + write!(&mut indented, "{}", self.body)?; Ok(()) } diff --git a/src/writers.rs b/src/writers.rs new file mode 100644 index 0000000..335f684 --- /dev/null +++ b/src/writers.rs @@ -0,0 +1,32 @@ +use std::fmt::{self, Display}; + +pub(crate) struct HeaderWriter<'a, H, W> { + pub(crate) inner: W, + pub(crate) header: &'a H, + pub(crate) started: bool, +} + +pub(crate) struct ReadyHeaderWriter<'a, 'b, H, W>(&'b mut HeaderWriter<'a, H, W>); + +impl<'a, H, W> HeaderWriter<'a, H, W> { + pub(crate) fn ready(&mut self) -> ReadyHeaderWriter<'a, '_, H, W> { + self.started = false; + + ReadyHeaderWriter(self) + } +} + +impl fmt::Write for ReadyHeaderWriter<'_, '_, H, W> +where + H: Display, + W: fmt::Write, +{ + fn write_str(&mut self, s: &str) -> fmt::Result { + if !self.0.started && !s.is_empty() { + self.0.inner.write_fmt(format_args!("{}", self.0.header))?; + self.0.started = true; + } + + self.0.inner.write_str(s) + } +}