Skip to content

Commit

Permalink
Add readme + screenshot + examples
Browse files Browse the repository at this point in the history
  • Loading branch information
nilehmann committed Sep 23, 2023
1 parent 6bb85ec commit 82c952b
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 36 deletions.
37 changes: 37 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ version = "0.1.0"
clap = { version = "4.3.3", features = ["derive"] }
regex = "1.8.1"
termcolor = "1.2.0"
termion = "2.0.1"
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Backtracetk

Backtracetk is a command line tool to print colorized Rust backtraces without the need to add extra
dependencies to your project.
It works by capturing the output of a process, detecting anything that looks like a backtrace, and then printing
it with colors to be easier on the eyes.
It also prints snippets of the code at each frame if it can find them in the file system.

## Installation

```bash
cargo install --git https://github.com/nilehmann/backtracetk
```

## Screenshot

![Screenshot](./screenshot.png)
3 changes: 3 additions & 0 deletions examples/assert_failed.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fn main() {
assert_eq!(1, 2);
}
12 changes: 12 additions & 0 deletions examples/panic_macro.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
fn fn3() {
let blah = 123;
panic!("{}", blah);
}

fn fn2() {
fn3();
}

fn main() {
fn2();
}
24 changes: 24 additions & 0 deletions examples/unwrap_result.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
fn fn3() {
fn4(); // Source printing at start the of a file ...
}

fn fn2() {
// sdfsdf
let _dead = 1 + 4;
fn3();
let _fsdf = "sdfsdf";
let _fsgg = 2 + 5;
}

fn fn1() {
fn2();
}

fn main() {
fn1();
}

fn fn4() {
// Source printing at the end of a file
"x".parse::<u32>().unwrap();
}
Binary file added screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
108 changes: 75 additions & 33 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pub struct Backtrace {

struct PanicInfo {
thread: String,
source: SourceInfo,
at: String,
message: Vec<String>,
}

Expand All @@ -35,10 +35,13 @@ impl Backtrace {
if self.frames.is_empty() {
return Ok(());
}
let frameno_width = self.frames.len().ilog10() as usize + 1;
let framnow = self.compute_frameno_width();
let linenow = self.compute_lineno_width();
let width = self.compute_width(framnow);
writeln!(out, "\n{:━^width$}", " BACKTRACE ")?;

for frame in self.frames.iter().rev() {
frame.render(out, frameno_width)?;
frame.render(out, framnow, linenow)?;
}

if let Some(panic_info) = &self.panic_info {
Expand All @@ -47,47 +50,90 @@ impl Backtrace {

writeln!(out)
}

fn compute_lineno_width(&self) -> usize {
// This is assuming we have 2 more lines in the file, if we don't, in the worst case we will
// print an unnecesary extra space for each line number.
self.frames
.iter()
.flat_map(|f| &f.source_info)
.map(|source_info| source_info.lineno + 3)
.max()
.unwrap_or(1)
.ilog10() as usize
}

fn compute_frameno_width(&self) -> usize {
self.frames.len().ilog10() as usize + 1
}

fn compute_width(&self, frameno_width: usize) -> usize {
let term_size = termion::terminal_size().unwrap_or((80, 0)).0 as usize;
self.frames
.iter()
.map(|f| f.width(frameno_width))
.max()
.unwrap_or(80)
.min(term_size)
}
}

impl Frame {
fn render(&self, out: &mut StandardStream, width: usize) -> io::Result<()> {
write!(out, "{:>width$}: ", self.frameno)?;
fn render(&self, out: &mut StandardStream, framenow: usize, linenow: usize) -> io::Result<()> {
write!(out, "{:>framenow$}: ", self.frameno)?;

out.set_color(ColorSpec::new().set_fg(Some(Color::Green)))?;
writeln!(out, "{}", self.function)?;
out.set_color(&ColorSpec::new())?;

if let Some(source_info) = &self.source_info {
source_info.render(out, width)?;
source_info.render(out, framenow, linenow)?;
}
Ok(())
}

fn width(&self, frameno_width: usize) -> usize {
usize::max(
frameno_width + 2 + self.function.len(),
self.source_info
.as_ref()
.map(|s| s.width(frameno_width))
.unwrap_or(0),
)
}
}

impl SourceInfo {
fn render(&self, out: &mut StandardStream, width: usize) -> io::Result<()> {
write!(out, "{:width$} at ", "")?;
fn render(&self, out: &mut StandardStream, framenow: usize, linenow: usize) -> io::Result<()> {
write!(out, "{:framenow$} at ", "")?;
writeln!(out, "{self}")?;
self.render_code(out, width)?;
self.render_code(out, framenow, linenow)?;
Ok(())
}

fn render_code(&self, out: &mut StandardStream, width: usize) -> io::Result<()> {
fn render_code(
&self,
out: &mut StandardStream,
framenow: usize,
linenow: usize,
) -> io::Result<()> {
let path = Path::new(&self.file);
if path.exists() {
let lineno = self.lineno - 1;
let file = File::open(path)?;
let reader = io::BufReader::new(file);
let viewport = reader
let viewport: Vec<_> = reader
.lines()
.enumerate()
.skip(lineno.saturating_sub(2))
.take(5);
.take(5)
.collect();

for (i, line) in viewport {
if i == lineno {
out.set_color(ColorSpec::new().set_bold(true))?;
}
write!(out, "{:width$} {} | ", "", i + 1)?;
write!(out, "{:framenow$} {:>linenow$} | ", "", i + 1)?;
writeln!(out, "{}", line?)?;
if i == lineno {
out.set_color(ColorSpec::new().set_bold(false))?;
Expand All @@ -96,6 +142,11 @@ impl SourceInfo {
}
Ok(())
}

/// Width without considering the source code snippet
fn width(&self, framenow: usize) -> usize {
framenow + self.file.len() + (self.lineno.ilog10() + self.colno.ilog10()) as usize + 9
}
}

impl std::fmt::Display for SourceInfo {
Expand All @@ -107,12 +158,11 @@ impl std::fmt::Display for SourceInfo {
impl PanicInfo {
fn render(&self, out: &mut StandardStream) -> io::Result<()> {
out.set_color(ColorSpec::new().set_fg(Some(Color::Red)))?;
writeln!(out, "thread '{}' panicked at {}", self.thread, self.source)?;
out.set_color(&ColorSpec::new())?;

writeln!(out, "thread '{}' panicked at {}", self.thread, self.at)?;
for line in &self.message {
writeln!(out, ">> {}", line)?;
writeln!(out, "{}", line)?;
}
out.set_color(&ColorSpec::new())?;
Ok(())
}
}
Expand All @@ -129,7 +179,7 @@ enum ParsedLine {
/// ```ignore
/// thread 'rustc' panicked at /rustc/b3aa8e7168a3d940122db3561289ffbf3f587262/compiler/rustc_errors/src/lib.rs:1651:9:
/// ```
ThreadPanic { thread: String, source: SourceInfo },
ThreadPanic { thread: String, at: String },
/// The begining of a trace starts with `stack backtrace:`
BacktraceStart,
/// The "header" of a frame containing the frame number and the function's name, e.g.,
Expand All @@ -142,13 +192,14 @@ enum ParsedLine {
/// at /rustc/b3aa8e7168a3d940122db3561289ffbf3f587262/compiler/rustc_middle/src/ty/context/tls.rs:79:9
/// ```
BacktraceSource(SourceInfo),
/// A line that doesn't match any patter
/// A line that doesn't match any pattern
Other(String),
}

impl Parser {
pub fn new() -> Parser {
let panic_regex = Regex::new(r"^thread\s+'(?P<thread>[^']+)'\spanicked\s+at\s+(?P<file>[^:]+):(?P<lineno>\d+):(?P<colno>\d+)").unwrap();
let panic_regex =
Regex::new(r"^thread\s+'(?P<thread>[^']+)'\spanicked\s+at\s+(?P<at>.+)").unwrap();
let function_regex =
Regex::new(r"^\s+(?P<frameno>\d+):\s+((\w+)\s+-\s+)?(?P<function>.+)").unwrap();
let source_regex =
Expand All @@ -166,17 +217,8 @@ impl Parser {
ParsedLine::BacktraceStart
} else if let Some(captures) = self.panic_regex.captures(&line) {
let thread = captures.name("thread").unwrap().as_str().to_string();
let file = captures.name("file").unwrap().as_str().to_string();
let lineno = captures.name("lineno").unwrap().as_str();
let colno = captures.name("colno").unwrap().as_str();
ParsedLine::ThreadPanic {
thread,
source: SourceInfo {
file,
lineno: lineno.parse().unwrap(),
colno: colno.parse().unwrap(),
},
}
let at = captures.name("at").unwrap().as_str().to_string();
ParsedLine::ThreadPanic { thread, at }
} else if let Some(captures) = self.function_regex.captures(&line) {
let frameno = captures.name("frameno").unwrap().as_str().to_string();
let function = captures.name("function").unwrap().as_str().to_string();
Expand Down Expand Up @@ -207,11 +249,11 @@ impl Parser {
let mut in_panic_info = false;
while let Some(line) = lines.next() {
match line {
ParsedLine::ThreadPanic { thread, source } => {
ParsedLine::ThreadPanic { thread, at } => {
in_panic_info = true;
panic_info = Some(PanicInfo {
thread,
source,
at,
message: vec![],
});
}
Expand Down
Loading

0 comments on commit 82c952b

Please sign in to comment.