[ ../ ](/)
# Brat: A Router for Unix Tools
If you've ever grown a shell script past a few hundred lines, you know the feeling. The `if`/`elif` chain at the top that dispatches on `$1`. The tangle of functions nobody dares reorder. The instinct to rewrite the whole thing in Python — even though it only ever shells out to `git`, `find`, and `tar`.
**Brat** is a different answer. It's a single-file POSIX shell script — about 120 lines of actual logic — that turns your growing script into a *program*: a front-end executable that routes to a collection of small commands, Git-style.
```
$ myprog build --release
↓ your shell finds myprog (Brat) somewhere in your PATH
bin/myprog build --release
↓ Brat extends PATH, finds myprog-build, and runs it
libexec/myprog-build --release
```
That's the whole idea. You drop Brat into `bin/myprog`, add tiny executables to `libexec/`, and it dispatches by name.
## Why bother?
Because each command becomes a separate process with its own arguments, standard I/O, and exit status. Your program turns into a composition of independently testable parts, and you never write another `case "$1" in` block.
There's no runtime to install, either — just a POSIX shell and the standard utilities (`awk`, `find`, `realpath`, `sort`, `tr`). The same script runs on Alpine, Debian, Fedora, FreeBSD, and macOS. And because Brat is *feature-complete*, you're not adopting a moving target: it's built to keep working unmodified for years.
## The layout
Brat infers everything from its own filename and location. A program named `myprog` looks like this:
```
myprog/
├── bin/
│ └── myprog # the router (Brat)
├── lib/
│ └── myprog/ # shared library files
│ ├── _init.sh # optional init hook
│ └── myprog.sh # shared helpers/aliases
└── libexec/ # command executables
└── myprog-
```
`libexec/` usually isn't on the `PATH` in an interactive shell, so your commands stay hidden. Users only see what's in `bin/`.
## Let's build one
Set up the skeleton:
```sh
mkdir myprog && cd myprog
mkdir -p bin lib/myprog libexec
curl -sL https://github.com/amadv/brat >bin/myprog
chmod +x bin/myprog
touch lib/myprog/_init.sh lib/myprog/myprog.sh
```
Add three aliases to `myprog.sh` so every command can reach Brat's shared helpers:
```sh
# lib/myprog/myprog.sh
alias self='"$SELF"'
alias error='self --- error "$0"'
alias usage='self --- usage "$0" "${USAGE:-}"'
```
Now a command. Save this as `libexec/myprog-hello` and make it executable:
```sh
#!/bin/sh
set -eu
. myprog.sh
echo "hello!"
```
```sh
chmod +x libexec/myprog-hello
```
Run it. Brat puts `lib/` first on `PATH`, which is why the bare `. myprog.sh` works:
```console
$ bin/myprog
usage: myprog
commands: hello
$ bin/myprog hello
hello!
```
Note that `hello` showed up in the command list automatically — no registration, no manifest.
## Arguments, without the ceremony
Brat takes a hands-off approach to argument syntax. It finds the command by skipping over anything that starts with a hyphen, then passes everything — before and after the command name — through in its original order.
| Invocation | Command | Arguments |
| --- | --- | --- |
| `myprog build a` | `build` | `a` |
| `myprog -v build a` | `build` | `-v a` |
| `myprog build -v a` | `build` | `-v a` |
| `myprog --file=out.txt build` | `build` | `--file=out.txt` |
| `myprog build --file out.txt` | `build` | `--file out.txt` |
The one gotcha: an option/value *pair* can't precede the command name, because `out.txt` looks like the command:
```console
$ myprog --file out.txt build
myprog: no such command: out.txt
```
Everything after the command name is fine.
## Errors and usage, the consistent way
Commands fail the same way everywhere, thanks to the aliases:
```sh
#!/bin/sh
set -eu
. myprog.sh
USAGE="deploy "
[ $# -eq 1 ] || usage
```
```console
$ bin/myprog deploy
usage: myprog deploy
```
The `USAGE` variable is just a convention — set it once, call `usage` with no arguments, and get a proper synopsis on standard error with a non-zero status.
## Grouping commands: namespaces
When a handful of commands belong together, group them under a namespace. A namespace is simply a symlink back to the router, plus a matching `lib/` directory:
```sh
ln -s ../bin/myprog libexec/myprog-env
mkdir -p lib/myprog-env
touch lib/myprog-env/_init.sh
```
Now `libexec/myprog-env-list`, `myprog-env-set`, and friends become commands of the `env` namespace:
```console
$ bin/myprog env
usage: myprog env
commands: list set
$ bin/myprog env list
...
```
Under the hood, `myprog env list` invokes the router a second time through the symlink (`NAME` becomes `myprog-env`), which then finds `myprog-env-list`. Nested routing, no special cases.
## Hiding helper executables
Any executable with two hyphens anywhere in its name is **private** — never dispatched to, never listed:
```
libexec/myprog-build--compile # internal helper for `myprog build`
libexec/myprog-build # the visible command
```
Since Brat puts `libexec/` on the `PATH`, `myprog-build` can call `myprog-build--compile` directly. You may be wondering how `myprog-build--compile` and `myprog-build` can coexist. They can't — the last one has to be named differently, e.g. `myprog-build-main`. The point is that commands can factor shared logic into internal executables without leaking them into the user-facing command list.
## Seeing what the router sees
When something isn't routing the way you expect, ask Brat directly with the special `---` argument:
```console
$ bin/myprog --- executables # every executable in the current namespace
$ bin/myprog --- env # the full environment commands run under
$ bin/myprog --- version # the content-addressed version
```
The same mechanism is what powers `error` and `usage` from inside commands — `"$SELF" --- error "$0" ...` calls back into the router's own helpers.
## When you're ready to grow
- **Commands can live anywhere in `PATH`.** Brat searches for `-` across the whole path, so a plugin can add commands without touching your tree.
- **Commands can be any language.** They're just executables. Shell is the default, not a requirement.
- **Set `VERSION` in `_init.sh`** to enable `myprog --version`.
The full annotated source *is* the manual — Brat is a literate program, with the documentation and the code interleaved in `brat.sh` and the manual generated from it. If you want to know exactly how routing works, read the source; the prose is right there next to the code it describes.
Brat is MIT-0 licensed. Grab it, rename it, ship it.