Skip to content

Commit

Permalink
Add option to specify a begin and end pattern
Browse files Browse the repository at this point in the history
  • Loading branch information
nilehmann committed Jul 7, 2024
1 parent 4e3f6eb commit 488c5d0
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 63 deletions.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ version = "0.1.0"
[dependencies]
anstream = "0.6.14"
anstyle = "1.0.7"
anyhow = "1.0.86"
clap = { version = "4.5.8", features = ["derive", "wrap_help"] }
regex = "1.10.5"
serde = { version = "1.0.203", features = ["derive"] }
termion = "4.0.2"
toml = "0.8.14"
toml = { version = "0.8.14", features = ["parse"] }
20 changes: 15 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
# Backtracetk

Backtracetk is a command-line tool that prints colorized Rust backtraces without needing extra dependencies. It works by capturing the output of a process, detecting anything that looks like a backtrace, and then printing it with colors to make it easier on the eyes. It also prints snippets of the code at each frame if it can find them in the filesystem.
Backtracetk is a command-line tool that prints colorized Rust backtraces without needing extra dependencies.
It works by capturing the output of a Rust binary, detecting anything that looks like a backtrace, and then printing it with colors to make it easier on the eyes.
Additionally, it displays code snippets if available in the filesystem and offers configurable options to hide specific frames.

Backtracetk is useful in situations where you can't or don't want to add runtime dependencies to your project. It is also more dynamic, allowing you to run the process many times (if it's cheap to do so) and adjust the output accordingly.
Backtracetk is useful in situations where you can't or don't want to add runtime dependencies.
It is thus more "dynamic", allowing you to run the process many times (assuming it's cheap to do so) and adjust the output accordingly without the need to recompile your code.

If you're okay with adding dependencies, consider looking at [color-eyre](https://crates.io/crates/color-eyre) or [color-backtrace](https://crates.io/crates/color-backtrace).
If you're ok with adding dependencies, consider looking at [color-eyre](https://crates.io/crates/color-eyre) or [color-backtrace](https://crates.io/crates/color-backtrace).

I've only tested this on Linux and primarily within a single project.
If you try it and encounter any issues, please share the output of your process.

## Installation

Expand Down Expand Up @@ -45,7 +51,11 @@ Options:
### Configuration
Backtracetk will attempt to locate a configuration file named `backtrack.toml` or `.backtrack.toml` in the parent directories starting from where the command is executed. Currently, the only supported configuration is `hide`, which accepts a list of regex patterns.
Any frame matching one of these patterns will be hidden from the output. For example:
Backtracetk will search for a configuration file named `backtrack.toml` or `.backtrack.toml` in the parent directories starting from the directory where the command is executed. Currently, the only supported configuration option is hide, which accepts an array of tables. Each table can take one of two forms:
* A table with the key `pattern` and a value containing a regex. Any frame matching this pattern will be hidden from the output.
* A table with the keys `start` and `end`, both containing regex values. Every frame between a frame matching the `start` regex and a frame matching the `end` regex will be hidden. The `end` pattern is optional; if omitted, every frame after matching `start` will be hidden.
For an example:
![Screenshot2](./screenshot2.png)
7 changes: 6 additions & 1 deletion backtracetk.toml
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
hide = ["core::panicking", "rust_begin_unwind"]
[[hide]]
pattern = "panic_macro::fn2"

[[hide]]
begin = "core::panicking"
end = "rust_begin_unwind"
Binary file modified screenshot2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 5 additions & 14 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub struct Backtrace {
}

impl Backtrace {
pub fn render(&self, filter: &mut impl Filter) -> io::Result<()> {
pub fn render(&self, filter: &mut impl FrameFilter) -> io::Result<()> {
if self.frames.is_empty() {
return Ok(());
}
Expand All @@ -32,7 +32,7 @@ impl Backtrace {

let mut hidden = 0;
for frame in self.frames.iter().rev() {
if filter.exclude(frame) {
if filter.should_hide(frame) {
hidden += 1;
} else {
print_hidden_frames_message(hidden, width)?;
Expand Down Expand Up @@ -218,7 +218,7 @@ enum ParsedLine {
/// at /rustc/b3aa8e7168a3d940122db3561289ffbf3f587262/compiler/rustc_middle/src/ty/context/tls.rs:79:9
/// ```
BacktraceSource(SourceInfo),
/// A line that doesn't match any pattern
/// A line that doesn't match any of the previous patterns
Other(String),
}

Expand Down Expand Up @@ -330,15 +330,6 @@ impl Parser {
}
}

pub trait Filter {
fn exclude(&mut self, frame: &Frame) -> bool;
}

impl<F> Filter for F
where
F: FnMut(&Frame) -> bool,
{
fn exclude(&mut self, frame: &Frame) -> bool {
(self)(frame)
}
pub trait FrameFilter {
fn should_hide(&mut self, frame: &Frame) -> bool;
}
212 changes: 170 additions & 42 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use std::fs;
use std::io::{self, BufRead, BufReader, Read};
use std::io::{BufRead, BufReader, Read};
use std::process::{Command, Stdio};

use backtracetk::Frame;
use backtracetk::{Frame, FrameFilter};
use clap::Parser;
use regex::Regex;
use serde::Deserialize;
Expand Down Expand Up @@ -65,10 +65,11 @@ impl BacktraceStyle {
}
}
}
fn main() -> io::Result<()> {
fn main() -> anyhow::Result<()> {
let mut args = Args::parse();

let config = read_config();
let config = Config::read()?;
let mut filters = config.to_filters()?;

let mut env_vars = vec![("RUST_BACKTRACE", args.style.env_var_str())];
if args.lib_backtrace.is_no() {
Expand Down Expand Up @@ -103,61 +104,188 @@ fn main() -> io::Result<()> {
parser.parse_line(line);
}

let mut filter = |frame: &Frame| {
for regex in &config.hide {
if regex.is_match(&frame.function) {
return true;
}
}
false
};
for backtrace in parser.into_backtraces() {
backtrace.render(&mut filter)?;
backtrace.render(&mut filters)?;
}

Ok(())
}

#[derive(Default, Deserialize)]
struct Config {
#[serde(deserialize_with = "deserialize_regex_vec")]
#[serde(default = "Default::default")]
hide: Vec<Regex>,
hide: Vec<HideConfig>,
}

fn read_config() -> Config {
let Some(path) = find_config_file() else {
return Config::default();
};
impl Config {
fn read() -> Result<Config, toml::de::Error> {
let Some(path) = Config::find_file() else {
return Ok(Config::default());
};

let mut contents = String::new();
let mut file = fs::File::open(path).unwrap();
file.read_to_string(&mut contents).unwrap();
toml::from_str(&contents).unwrap()
}
let mut contents = String::new();
let mut file = fs::File::open(path).unwrap();
file.read_to_string(&mut contents).unwrap();
toml::from_str(&contents)
}

fn find_config_file() -> Option<std::path::PathBuf> {
let mut path = std::env::current_dir().unwrap();
loop {
for name in ["backtracetk.toml", ".backtracetk.toml"] {
let file = path.join(name);
if file.exists() {
return Some(file);
fn to_filters(&self) -> Result<Filters, regex::Error> {
let mut filters = vec![];
for filter in &self.hide {
filters.push(filter.try_into()?)
}
Ok(Filters { filters })
}

fn find_file() -> Option<std::path::PathBuf> {
let mut path = std::env::current_dir().unwrap();
loop {
for name in ["backtracetk.toml", ".backtracetk.toml"] {
let file = path.join(name);
if file.exists() {
return Some(file);
}
}
if !path.pop() {
return None;
}
}
if !path.pop() {
return None;
}
}

enum HideConfig {
Pattern { pattern: String },
Span { begin: String, end: Option<String> },
}

pub struct Filters {
filters: Vec<Filter>,
}

impl FrameFilter for Filters {
fn should_hide(&mut self, frame: &Frame) -> bool {
self.filters
.iter_mut()
.any(|filter| filter.do_match(&frame.function))
}
}

enum Filter {
Pattern(Regex),
Span {
begin: Regex,
end: Option<Regex>,
in_section: bool,
},
}

impl Filter {
fn do_match(&mut self, s: &str) -> bool {
match self {
Filter::Pattern(regex) => regex.is_match(s),
Filter::Span {
begin: start,
end,
in_section,
} => {
if *in_section {
let Some(end) = end else {
return true;
};
if end.is_match(s) {
*in_section = false;
}
true
} else {
if start.is_match(s) {
*in_section = true;
}
*in_section
}
}
}
}
}

fn deserialize_regex_vec<'de, D>(deserializer: D) -> Result<Vec<Regex>, D::Error>
where
D: serde::Deserializer<'de>,
{
let strings: Vec<String> = Vec::deserialize(deserializer)?;
strings
.into_iter()
.map(|s| Regex::try_from(s).map_err(serde::de::Error::custom))
.collect()
impl TryFrom<&HideConfig> for Filter {
type Error = regex::Error;

fn try_from(value: &HideConfig) -> Result<Self, Self::Error> {
let filter = match value {
HideConfig::Pattern { pattern } => Filter::Pattern(pattern.as_str().try_into()?),
HideConfig::Span { begin, end } => Filter::Span {
begin: begin.as_str().try_into()?,
end: end.as_deref().map(Regex::try_from).transpose()?,
in_section: false,
},
};
Ok(filter)
}
}

impl<'de> Deserialize<'de> for HideConfig {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;

struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = HideConfig;

fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
formatter,
"a map with the field `pattern`, or a map with the fields `start` and an optional `end`"
)
}

fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
let unexpected = |k| A::Error::custom(format!("unexpected field `{k}`"));
let (k1, v1) = map
.next_entry::<String, String>()?
.ok_or_else(|| A::Error::custom("missing field `pattern` or `start`"))?;

match &*k1 {
"pattern" => {
if let Some(k2) = map.next_key::<String>()? {
return Err(unexpected(k2));
}
Ok(HideConfig::Pattern { pattern: v1 })
}
"begin" => {
let Some((k2, v2)) = map.next_entry::<String, String>()? else {
return Ok(HideConfig::Span {
begin: v1,
end: None,
});
};
(k2 == "end")
.then(|| HideConfig::Span {
begin: v1,
end: Some(v2),
})
.ok_or_else(|| unexpected(k2))
}
"end" => {
let (k2, v2) = map
.next_entry::<String, String>()?
.ok_or_else(|| A::Error::missing_field("begin"))?;
(k2 == "begin")
.then(|| HideConfig::Span {
begin: v2,
end: Some(v1),
})
.ok_or_else(|| unexpected(k2))
}
_ => Err(unexpected(k1)),
}
}
}
deserializer.deserialize_map(Visitor)
}
}

0 comments on commit 488c5d0

Please sign in to comment.