Brutal Runner for Automated Tests, a parallel TAP testing harness for the POSIX shell
  • Shell 77.5%
  • Awk 22.5%
Find a file
Sam Stephenson 94e95bcb9e
All checks were successful
Brat CI / macOS (push) Successful in 6s
Brat CI / Linux (alpine) (push) Successful in 8s
Brat CI / Linux (debian) (push) Successful in 22s
Brat CI / Linux (fedora) (push) Successful in 28s
Brat CI / FreeBSD (push) Successful in 1m1s
Fix per-user installation instructions
2026-02-20 12:47:31 -06:00
.forgejo/workflows Move CI runner into a top-level script/test 2026-02-18 13:57:51 -06:00
bin brut: Pass original arguments to _unhandled.sh 2026-02-06 13:21:22 -06:00
lib/brat Brat 0.9.0 2026-02-19 12:48:02 -06:00
libexec Escape # and \ in test names, per the TAP spec 2026-02-13 18:38:36 -06:00
script Test suite defaults BRAT_JOBS to the number of CPUs 2026-02-18 15:50:04 -06:00
test Extract script/loc 2026-02-18 13:59:25 -06:00
.gitattributes Mark wcwidth.awk as vendored 2026-02-01 22:13:52 -06:00
CHANGELOG.md Brat 0.9.0 2026-02-19 12:48:02 -06:00
LICENSE.md Initial commit 2026-01-22 11:17:18 -06:00
README.md Fix per-user installation instructions 2026-02-20 12:47:31 -06:00

Brat

Brat is the Brutal Runner for Automated Tests, a parallel TAP testing harness for the POSIX shell.

Looping screen recording of Brat running its own test suite

Brutal as in architecture. Brat is true to the “materials” it is built with: shell, awk, and the Unix pipeline. It reveals its internal plumbing in the same way a brutalist building might expose its ductwork. Some will find it ugly; others (maybe you?) will appreciate its didactic honesty.

POSIX as in zero dependencies. Brat targets the POSIX.1-2024 specification. Practically speaking, it is designed to run on the minimum common subset of contemporary Unix OSes, with no other dependencies or specific implementation requirements. We test Brat under continuous integration against a variety of platforms.

Intentionally small. Brat is designed to be embedded directly into your project. It has no build step and nothing to configure. At just under a thousand lines of shell and awk, you can read and understand the codebase in an afternoon.


Jump to: Installation | Writing Tests | Running Tests | Implementation Notes | Contributing | License



Overview

With Brat, you write tests for Unix programs using a special shell syntax:

# test/backup.brat

setup() {
  cd "$DIR/.."
}

@test "prints usage when run without arguments" {
  run bin/backup.sh
  [ $status -eq 1 ]
  match "$stderr" 'usage:'
}

@test "errors when source directory does not exist" {
  run bin/backup.sh /nonexistent "$TEST_TMP.tar.gz"
  [ $status -eq 1 ]
  match "$stderr" 'not found'
}

@test "creates backup archive" {
  run bin/backup.sh "$DIR/fixtures/testdata" "$TEST_TMP.tar.gz"
  [ $status -eq 0 ]
  tar tf "$TEST_TMP.tar.gz"
}

A preprocessor transforms these test cases into shell functions which run with set -eu (exit on error, error when referencing undefined variables). In this way, every line of a test case acts as an assertion.

When you run your tests, Brat displays the results in a streaming TAP format:

$ brat test/*.brat
TAP version 14
1..3

ok 1 - backup.brat:7: prints usage when run without arguments
ok 2 - backup.brat:13: errors when source directory does not exist
ok 3 - backup.brat:19: creates backup archive

#  ✓ 3 tests (3 passed, 0 failed, 0 skipped)

If any line of a test fails, Brat shows the shell’s xtrace (set -x) output up to that point, along with anything written to stdout or stderr:

$ brat test/backup.brat:19
TAP version 14
1..1

not ok 1 - backup.brat:19: creates backup archive
#  + setup
#  + cd $DIR/..
#  + run bin/backup.sh $DIR/fixtures/testdata $TEST_TMP.tar.gz
#  + '[' 0 -eq 0 ']'
#  + tar tf $TEST_TMP.tar.gz
#  tar: Error opening archive: Unrecognized archive format
#  (test failed with status 1)

#  ✘ 1 test (0 passed, 1 failed, 0 skipped)

Brat formats its TAP stream with color when connected to a terminal. In particular, failing tests are highlighted in red.


Parallel Execution

Tests run sequentially by default, but Brat has built-in support for parallel test execution. Use -j or set $BRAT_JOBS to run tests in parallel. For example, to run up to 8 tests concurrently:

$ brat -j 8 test/*.brat

Brat spawns each test in a background process, streaming results as they complete, potentially out of order. The pretty formatter buffers and sorts them live for display.


Comparison with Bats

Brat is a spiritual successor to Bats, the Bash Automated Testing System. If you’ve used Bats, Brat will feel familiar, but more spartan.

Bats Brat
Shell Requires Bash Works with any POSIX shell
Parallel execution Requires GNU parallel Built-in support, using a FIFO
Output TAP or a proprietary pretty format TAP always; pretty format is highlighted and sorted TAP
Output capture $output, $lines[] (in-memory strings) $stdout, $stderr (file paths)
Built-in helpers Rich standard library and ecosystem Minimal
Lifecycle hooks Per-test and per-module setup and teardown Per-test setup and teardown only

One important difference is that Brat’s run helper captures output to separate files and exposes their paths to you, avoiding the runtime overhead of reading large outputs into strings and arrays.


Portability

Brat is written entirely in POSIX shell and awk, targeting the POSIX.1-2024 standard with no other dependencies. It is architecture-independent and does not require a C compiler.

We test Brat, using Brat, with continuous integration on the following platforms:

sh awk
Alpine Linux busybox ash busybox awk
Debian Linux dash mawk
Fedora Linux Bash gawk
FreeBSD FreeBSD ash nawk
macOS Bash (3.2) nawk

Build Status



Installation

Brat has no build step and no dependencies to install.


Installing Brat Globally

Download and extract the latest release archive and symlink bin/brat into your PATH. For example, to install Brat in /usr/local:

# curl -sL https://codeberg.org/sstephenson/brat/archive/latest.tar.gz | tar -C /usr/local -xf -
# ln -s /usr/local/brat/bin/brat /usr/local/bin/brat

Or if you prefer a per-user installation (assuming $HOME/.local/bin is in your PATH):

$ curl -sL https://codeberg.org/sstephenson/brat/archive/latest.tar.gz | tar -C ~/.local -xf -
$ ln -s ~/.local/brat/bin/brat ~/.local/bin/brat

Embedding Brat in Your Project

Clone Brat into your project and run it directly:

$ git clone https://codeberg.org/sstephenson/brat.git vendor/brat
$ vendor/brat/bin/brat test/*.brat



Writing Tests

Test files use the .brat extension by convention. Each file is a shell script containing one or more test definitions:

@test "description of what this tests" {
  # Commands here run with errexit enabled; any
  # command that exits nonzero fails the test
  [ 1 -eq 1 ]
}

It’s a good idea to add a standard #!/bin/sh shebang to the top of each test file so that your editor or code forge applies proper syntax highlighting. Note, however, that Brat test files cannot be executed directly by the shell.


About the Test Environment

Brat automatically sets the following variables before each test run:

  • $FILE — the path to the test file
  • $DIR — the directory containing the test file
  • $TEST_TMP — a unique temporary path prefix for the current test

Use the $DIR variable to source test helper scripts or load fixture data relative to the location of the test file.

You can use the $TEST_TMP variable as a prefix for temporary files or directories you create during a test. Filenames matching $TEST_TMP.* are automatically deleted after each test run.


Running Commands with the run Helper

Use run to execute a command and capture its output and status code:

@test "captures exit status and output" {
  run ls /nonexistent
  [ $status -eq 1 ]
  match "$stderr" 'No such file'
}

After run, three variables are available:

  • $status — the command’s exit code
  • $stdout — the path to a file containing standard output
  • $stderr — the path to a file containing standard error

Matching Output with the match Helper

Use match to assert that a file contains a string or pattern:

match "$stdout" 'hello world'   # exact substring
match "$stdout" '/^hello .+$/'  # ERE pattern

If the second argument begins and ends with a /, the match helper treats it as an extended regular expression (ERE) pattern. Otherwise, it is treated as an exact substring to match.


Comparing Files with the compare Helper

Use compare to assert that two files have identical contents:

run my_formatter <input.txt
compare "$stdout" "$DIR/fixtures/expected-output.txt"

compare uses the POSIX cksum utility to calculate a 32-bit CRC of both files and compare them, along with the files’ lengths, to determine equivalence. Do not use this helper if you need cryptographic integrity when comparing files. It is provided as an approximate replacement for cmp on systems where that command is not included by default.


Skipping Tests

Use @skip to mark tests that shouldn’t run:

@skip "not yet implemented" {
  # This test body is not executed
  false
}

Brat treats a @skip test like a passing test. It will appear with a # SKIP directive following its name in the TAP output.


Marking Works in Progress

Use @todo to mark tests you expect to fail:

@todo "waiting on upstream fix" {
  # Runs, but records as passing even when it fails
  run buggy_command
  [ $status -eq 0 ]
}

If a @todo test fails, Brat will display its xtrace output but otherwise treat it as a passing test. It will appear with a # TODO directive following its name in the TAP output.


Lifecycle Hooks

You can define setup and teardown functions to run code before and after each test case:

setup() {
  TMPFILE="$(mktemp)"
}

teardown() {
  rm -f "$TMPFILE"
}

@test "uses the temp file" {
  echo data >"$TMPFILE"
  [ -s "$TMPFILE" ]
}

teardown runs even when a test fails, so it’s safe to use for cleanup.


Top-Level Code

Code outside of @test, @skip, and @todo blocks runs twice: once when Brat scans the file to discover tests and their names, and again before each test runs.

# Runs during both planning and test execution
cd "$DIR/.."

setup() {
  # Only called during test execution
}

@test "example" {
  # ...
}

Keep this in mind if your top-level code has side effects. In practice, most test files only define functions (like setup) at the top level, which is harmless during planning.



Running Tests

To… Run…
Run all tests in a directory brat test/*.brat
Run a specific test file brat test/backup.brat
Run a specific test by line number brat test/backup.brat:19
Run tests in parallel (8 concurrent jobs) brat -j 8 test/*.brat
Filter tests by exact name match brat -n "creates backup archive" test/*.brat
Filter tests by extended regular expression (ERE) brat -n "/backup/" test/*.brat
Exclude tests by exact name or ERE brat -e "/usage/" test/*.brat

Working with Subcommands

Brat exposes the subcommands that make up its internal pipeline. When you run brat test/*.brat, Brat orchestrates the following:

  1. For each test file, test-plan extracts test metadata (file, line, kind, name) into a tab-delimited plan.
  2. plan-build aggregates these plans, sorts the tests by filename and line number, and applies any -n/-e filters.
  3. plan-run executes tests from the plan, invoking test-run in parallel and printing the results as TAP.

You can work with this plumbing directly:

Subcommand Description
brat plan-run The main entry point: build a plan, run tests, format output
brat plan-build Build a test plan from files, applying -n/-e filters
brat test-plan Extract test metadata from a single file
brat test-run Run a single test by file and line number

The subcommands are composable. For example, you can build a plan once and pipe it to brat plan-run -:

$ brat plan-build test/*.brat | grep backup | sort -r >plan.txt
$ brat plan-run - <plan.txt

Formatting Output

Brat outputs TAP version 14. When connected to a terminal, the TAP stream passes through a pretty formatter that live-sorts results and applies syntax highlighting. When stdout is not a terminal, or when $CI is set, Brat outputs unadorned TAP.

You can force raw TAP output by setting BRAT_FORMAT=plain.



Writing Brat, Biblioteca Vasconcelos, Ciudad de México, 2026.



Implementation Notes

Brat is built on a small command dispatcher called Brut, the Brutal Router for Unix Tools, which discovers and delegates to subcommand executables in the libexec/ directory. Brut is entirely self-contained in the bin/brat script.


Dispatch Behavior

Before parsing any arguments, bin/brat sources lib/brat/_init.sh, which forks a copy of itself to continue subcommand execution, waits on the forked process to exit, and deletes any temporary files it created. This automatic garbage collection removes the need for bookkeeping in subcommands.

After scanning arguments, if bin/brat does not find a matching subcommand, it sources lib/brat/_unhandled.sh, which attempts to rewrite the arguments into a brat plan-run pipeline. See Working with Subcommands for details on the default pipeline.


Subcommand Interaction

Brat locates itself in the filesystem and adds its libexec/ and lib/brat/ directories to the front of the PATH. Subcommands invoke each other directly (e.g. brat-plan-build, brat-test-run) without going through the dispatcher. Subcommands with -- in the name (e.g. brat--tap-format) are “private” and cannot be invoked as arguments to bin/brat.


Shell Functions

Shared shell functions live in lib/brat/. Because this directory is first in the PATH, its files can be sourced directly by subcommands (e.g. . brat.sh).

  • lib/brat/brat.sh — caching, preprocessing, EXIT trap chaining; sourced by convention at the top of every subcommand script
  • lib/brat/eval.sh — test environment and lifecycle functions; sourced by brat-test-plan and brat-test-run
  • lib/brat/test.sh — the run, match, and compare helpers; sourced by brat-test-run

Awk Filters

Brat’s many awk filters also live in lib/brat/.


Tracing Execution

You can set BRAT_DEBUG=1 to follow the execution of a Brat run. When this variable is set, the lib/brat/_init.sh script enables xtrace output to stderr with set -x. Note that this does not include the trace output for tests themselves, which is redirected to disk by Brat.



Contributing

Brat is hosted on Codeberg: https://codeberg.org/sstephenson/brat

We welcome issues and tested pull requests from human contributors. However, before submitting a large pull request, or one that changes behavior that is not a bug, we ask that you please open an issue first so we can discuss whether it is a good fit for the project.


About the Test Suite

Brat’s tests live in the test/ directory; the test/*.brat files together comprise its test suite. The tests in these files primarily invoke Brat on another tree of test files rooted in test/fixtures/.

Use the script/test command to run the test suite. This script first performs a series of “sentinel” checks to verify that Brat actually runs tests and propagates their exit statuses. Then it runs bin/brat test/*.brat in parallel, with a job count equal to the number of CPUs on the host system.


Reporting Issues

Brat is portable software and compatibility is a moving target. When reporting issues, please be sure to include information about your operating system, including its release version, and the versions and lineage of the sh, awk, and sed commands.


Code Conventions

When contributing changes to Brat, please respect the conventions of existing code in lib/brat/ and libexec/.

Shell should be written with set -eu and careful consideration of what is specified by POSIX. See the Shell Command Language specification for more details.

Similarly, awk code should be written in the subset specified by POSIX. Be sure to declare local variables in awk functions at the end of the parameter list. By convention, Brat separates “real” parameters from local variables with an unused parameter named __.



License

Brat is free software, distributable under the terms of the MIT + Trans Rights License. See LICENSE.md for details.

Brat includes a copy of wcwidth.awk by Eric Pruitt, released under the 2-Clause BSD license.


© 2026 Sam Stephenson. Handwritten in Mexico City.