Created
February 2, 2026 21:40
-
-
Save agavra/69b8d2eed01cd299a6abd42cfc697a79 to your computer and use it in GitHub Desktop.
rust backtrace table formatter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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