Skip to content

Instantly share code, notes, and snippets.

@agavra
Created February 2, 2026 21:40
Show Gist options
  • Select an option

  • Save agavra/69b8d2eed01cd299a6abd42cfc697a79 to your computer and use it in GitHub Desktop.

Select an option

Save agavra/69b8d2eed01cd299a6abd42cfc697a79 to your computer and use it in GitHub Desktop.
rust backtrace table formatter
#!/bin/bash
# Rust Backtrace Formatter
# Usage: format-backtrace [-v] [-vv]
# (no flag): show short function name only
# -v: show truncated full path with ... prefix
# -vv: show complete full path
SCRIPT_DIR="$(dirname "$0")"
VERBOSE=0
for arg in "$@"; do
case "$arg" in
-vv)
VERBOSE=2
;;
-v)
VERBOSE=1
;;
esac
done
awk -v verbose="$VERBOSE" -f "$SCRIPT_DIR/format-backtrace-impl.awk"
#!/usr/bin/awk -f
# Rust Backtrace Formatter
# Parses GDB-style backtraces and outputs readable tables
# Compatible with POSIX awk (macOS/BSD)
BEGIN {
in_thread = 0
header_printed = 0
row_num = 0
# ANSI escape codes for alternating row colors
bg_alt = "\033[48;5;236m" # subtle dark gray background
reset = "\033[0m"
}
# Thread header: Thread N (Thread 0x... (LWP ...) "name"):
# Handle optional leading whitespace
/^[ \t]*Thread [0-9]+/ {
# Print previous thread's content if any
if (in_thread && header_printed) {
print ""
}
# Extract thread number - $2 is the number after "Thread"
thread_num = $2
# Extract thread name from quotes if present
thread_name = ""
if (match($0, /"[^"]*"/)) {
thread_name = substr($0, RSTART+1, RLENGTH-2)
}
if (thread_name != "") {
printf "Thread %s (%s):\n", thread_num, thread_name
} else {
printf "Thread %s:\n", thread_num
}
# Print table header
if (verbose > 0) {
printf " %3s | %-20s | %-20s | %5s | %-30s | %s\n", "#", "Library", "File", "Line", "Function", "Full Path"
printf " %s-+-%s-+-%s-+-%s-+-%s-+-%s\n", "---", "--------------------", "--------------------", "-----", "------------------------------", "----------------------------------------------------"
} else {
printf " %3s | %-20s | %-20s | %5s | %s\n", "#", "Library", "File", "Line", "Function"
printf " %s-+-%s-+-%s-+-%s-+-%s\n", "---", "--------------------", "--------------------", "-----", "----------------------------------------------------"
}
in_thread = 1
header_printed = 1
row_num = 0
next
}
# Frame line: #N 0x... in function::path (args) at /path/to/file.rs:line
# Or: #N function () at /path/to/file.rs:line
# Handle optional leading whitespace
/^[ \t]*#[0-9]+/ {
# If we haven't printed a header yet (no Thread header seen), print one now
if (!header_printed) {
printf "Backtrace:\n"
if (verbose > 0) {
printf " %3s | %-20s | %-20s | %5s | %-30s | %s\n", "#", "Library", "File", "Line", "Function", "Full Path"
printf " %s-+-%s-+-%s-+-%s-+-%s-+-%s\n", "---", "--------------------", "--------------------", "-----", "------------------------------", "----------------------------------------------------"
} else {
printf " %3s | %-20s | %-20s | %5s | %s\n", "#", "Library", "File", "Line", "Function"
printf " %s-+-%s-+-%s-+-%s-+-%s\n", "---", "--------------------", "--------------------", "-----", "----------------------------------------------------"
}
header_printed = 1
row_num = 0
}
# Strip leading whitespace from line for processing
line = $0
gsub(/^[ \t]+/, "", line)
# Extract frame number - find #N at start
if (match(line, /^#[0-9]+/)) {
frame_num = substr(line, 2, RLENGTH - 1)
} else {
frame_num = "?"
}
# Extract function name - find " in " and then go until " (" or " at "
func_name = ""
# First, find the start position after " in "
if (match(line, / in /)) {
start_pos = RSTART + 4
rest = substr(line, start_pos)
# Find where function ends: before " (" that's followed by args, or " at "
# For functions with generics like <T as Trait>::func, we need to track angle brackets
func_end = 0
angle_depth = 0
paren_depth = 0
for (i = 1; i <= length(rest); i++) {
c = substr(rest, i, 1)
if (c == "<") angle_depth++
else if (c == ">") angle_depth--
else if (c == "(") {
if (angle_depth == 0) {
func_end = i - 1
break
}
paren_depth++
}
else if (c == ")") paren_depth--
}
if (func_end > 0) {
func_name = substr(rest, 1, func_end)
# Trim trailing space
gsub(/ +$/, "", func_name)
} else {
# Fallback: try to find " at " as the boundary
if (match(rest, / at /)) {
func_name = substr(rest, 1, RSTART - 1)
} else {
func_name = rest
}
}
} else {
# Handle case without "in" keyword (like syscall)
# Find the function name between frame# and ()
# Line has been stripped of leading whitespace, so it starts with #N
if (match(line, /^#[0-9]+[ \t]+[^ (]+/)) {
func_name = substr(line, RSTART, RLENGTH)
gsub(/^#[0-9]+[ \t]+/, "", func_name)
}
}
# Simplify function name
# Remove hash suffix like ::h1a2b3c4d
gsub(/::h[0-9a-f]+$/, "", func_name)
# Replace {impl#N} with {impl}
gsub(/\{impl#[0-9]+\}/, "{impl}", func_name)
# Replace {closure#N} with {closure}
gsub(/\{closure#[0-9]+\}/, "{closure}", func_name)
# Replace {closure_env#N} with {closure}
gsub(/\{closure_env#[0-9]+\}/, "{closure}", func_name)
# Replace {{closure}} with {closure}
gsub(/\{\{closure\}\}/, "{closure}", func_name)
# Save full function name for library extraction
full_func_name = func_name
# Always extract just the function name (last segment before generics) for Function column
func_base = full_func_name
if (match(func_base, /<.*/)) {
func_base = substr(func_base, 1, RSTART - 1)
}
n_segments = split(func_base, segments, "::")
if (n_segments > 1) {
func_name = segments[n_segments]
} else {
func_name = func_base
}
# Prepare full path column based on verbose level
# verbose=1: truncated with ... prefix
# verbose=2: complete full path
if (verbose == 1 && length(full_func_name) > 60) {
full_path_col = "..." substr(full_func_name, length(full_func_name) - 56)
} else {
full_path_col = full_func_name
}
# Extract file path and line number
file_name = "-"
line_num = "-"
library = "-"
if (match(line, / at [^ ]+:[0-9]+$/)) {
path_and_line = substr(line, RSTART+4, RLENGTH-4)
# Split path:line
n = split(path_and_line, parts, ":")
if (n >= 2) {
line_num = parts[n]
# Reconstruct path (in case path contains colons on Windows)
full_path = parts[1]
for (i = 2; i < n; i++) {
full_path = full_path ":" parts[i]
}
# Extract basename
n_path = split(full_path, path_parts, "/")
file_name = path_parts[n_path]
# Extract library/crate name from cargo path
# Pattern: .cargo/registry/src/.../crate-version/src/...
if (match(full_path, /\.cargo\/registry\/src\/[^\/]+\/[^\/]+/)) {
crate_with_version = substr(full_path, RSTART, RLENGTH)
# Get last component (crate-version)
n_crate = split(crate_with_version, crate_parts, "/")
crate_version = crate_parts[n_crate]
# Remove version suffix (everything from -N.N onwards)
library = crate_version
gsub(/-[0-9]+\.[0-9]+.*$/, "", library)
} else if (match(full_path, /\.cargo\/git\/checkouts\/[^\/]+/)) {
# Git checkout - extract crate name
crate_path = substr(full_path, RSTART, RLENGTH)
n_crate = split(crate_path, crate_parts, "/")
library = crate_parts[n_crate]
# Remove hash suffix if present
gsub(/-[a-f0-9]+$/, "", library)
} else if (full_func_name != "" && match(full_func_name, /::/)) {
# Fallback: use first segment of function name as library
split(full_func_name, func_parts, "::")
library = func_parts[1]
}
}
}
# Truncate file name if too long
if (length(file_name) > 20) {
file_name = substr(file_name, 1, 17) "..."
}
# Truncate library name if too long
if (length(library) > 20) {
library = substr(library, 1, 17) "..."
}
# Print formatted row with alternating background
row_num++
if (verbose > 0) {
if (row_num % 2 == 0) {
printf "%s %3s | %-20s | %-20s | %5s | %-30s | %s%s\n", bg_alt, frame_num, library, file_name, line_num, func_name, full_path_col, reset
} else {
printf " %3s | %-20s | %-20s | %5s | %-30s | %s\n", frame_num, library, file_name, line_num, func_name, full_path_col
}
} else {
if (row_num % 2 == 0) {
printf "%s %3s | %-20s | %-20s | %5s | %s%s\n", bg_alt, frame_num, library, file_name, line_num, func_name, reset
} else {
printf " %3s | %-20s | %-20s | %5s | %s\n", frame_num, library, file_name, line_num, func_name
}
}
next
}
# Skip other lines but track if we're in a thread
{
# Continue silently
}
END {
if (header_printed) {
print ""
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment