Note
- This readme was auto generated by claude
- Another (huge) auto generated documentation by deepwiki
baj is a compiler that turns declarative sources (YAML, Markdown) into a single self-contained Bash script — sourceable, executable, and streamable over SSH without copying files.
source <(curl -fsSL https://raw.githubusercontent.com/thydel/baj/2025-12-22-a/cmd/baj.sh)
path:addp ~/binA Bash function lives in memory. Unlike a script, it can be:
- serialized with
declare -f - transmitted over a pipe
- evaluated by a remote shell with zero filesystem footprint
baj exploits this: the unit of work is the function, not the file. A set of functions can be selected, serialized, and streamed:
with-lib path git2md -- gr2md | ssh host bashNothing is installed. Nothing persists. The remote shell evaluates functions from stdin, runs them, and exits.
jq is typically used to query JSON. baj uses it as a general-purpose transformation language across the entire pipeline:
- Parse: Pandoc AST (JSON) → structured function records
- Classify: tag each record as
sub,sup,root, oralien - Inherit: deep-merge superclass records into subclasses (scalars override, arrays concatenate) — declarative OOP on plain data
- Emit: lower function records into Bash source (
ns:fun () { ... }, aliases, variables, namespace helpers)
jq also appears inside generated functions. A YAML key jq: becomes
a local jq='...' variable, and the function body calls jq "$jq" —
the function carries its own jq program as data.
YAML / Markdown
│
▼
asjs (yq) → flat JSON array of objects
│
▼
bm4 (jq → m4 -P) → textual macro expansion (before inheritance)
│
▼
dist (jq) → classification + deep-merge inheritance
│
▼
emit (jq) → Bash functions, aliases, namespace helpers
m4 runs before inheritance so expanded text is inherited like any
other value. Macros use French guillemets (« ») as quotes and are
namespace-prefixed — no accidental expansion.
Bash resolves aliases at parse time, not execution time. baj uses this as a source-level indirection:
- YAML sources use short names:
addp - baj generates
alias addp=path:addp - when Bash parses the output,
addpis replaced bypath:addp - the final in-memory code contains only qualified names
This means you can change a function's namespace without editing its source. Aliases are a compilation mechanism, not sugar.
The same generated file works two ways because it ends with eval "$@":
| Mode | Usage | What happens |
|---|---|---|
| Library | source baj.sh |
Functions and aliases loaded into current shell |
| Command | ./baj.sh path:addp ~/bin |
Arguments evaluated as a function call |
baj compiles itself in two stages:
-
Core (
baj:init) —baj-boot.sh(hand-written, no macros) readsbaj-core.md(literate Bash+jq in Markdown). This produces the core functions:baj:asjs,baj:bm4,baj:dist,baj:emit. -
Library (
baj:main) — the core processesbaj-lib.ymlthrough its own pipeline (including m4), producing the fullbajl:*toolkit:load,with-lib,as-cmd,nss, etc.
The core cannot use m4 (it defines the m4 processor). The library uses m4 freely. This breaks the circular dependency.
Input — lib/path.yml:
- id:
ns: path
- id:
sh: ': ${1:?}; PATH=$(jq "$jq" -nr --args "$@")'
- m4:
EP: '(env.PATH / ":") as $p'
- id: addp
jq: 'EP | ($ARGS.positional - $p) + $p | join(":")'
- id: delp
jq: 'EP | $p - $ARGS.positional | join(":")'Output — cmd/path.sh (after make):
path:addp () {
local jq='(env.PATH / ":") as $p
| ($ARGS.positional - $p) + $p | join(":")';
: ${1:?}; PATH=$(jq "$jq" -nr --args "$@")
}What happened:
EPwas m4-expanded into the jq expression- The root
sh:(with noid) was distributed to bothaddpanddelp jq:becamelocal jq='...'- The function calls
jq "$jq"directly — running its own jq program against$PATH
Consider idempotent PATH editing. In jq:
env.PATH / ":" | . - ["/home/thy/bin"] + ["/home/thy/bin"] | join(":")Split on :, remove then prepend — idempotency falls out of array
semantics. No edge-case string matching, no regex. Call it twice, same
result.
This is a powerful one-liner, but it's not directly usable. The
classic Bash alternative — case ":$PATH:" in *:"$1":*) ;; — works
but can't express set operations this cleanly. A Python script would
handle it trivially but adds a runtime dependency disproportionate to
the task.
baj sits in the sweet spot: it turns that jq expression into a callable, self-contained tool with no dependency beyond bash and jq (both ubiquitous). The YAML source declares the jq program, baj compiles it into a function that carries its own jq logic as data:
source path.sh
path:addp ~/bin # fully qualified — always unambiguous
addp ~/bin # short alias — works unless another lib claimed it
path:delp ~/old/bin # same mechanism, different jq expressionAliases aren't only a compile-time mechanism — they survive into the
interactive shell. After sourcing, both addp and path:addp work.
The qualified form is always safe; the short form is convenient when
there's no collision with another sourced library.
The function runs jq internally, captures the result, assigns it to
$PATH. Two lines of YAML produced a reusable, composable, SSH-streamable
tool — from a one-liner that would otherwise live in someone's dotfiles
and never be shared.
./path.sh path:addp ~/bin runs in command mode — but it can't
work. A script runs in a subshell: PATH is modified in the child
process and lost when it exits. Only sourced functions operate in the
current shell:
source path.sh
path:addp ~/bin # modifies PATH in the current shellThis is the classic argument for bags of functions over scripts. A script is an isolated world; a sourced function is a tool that acts on your environment.
baj functions carry their own resolved source. You can inspect exactly what the generator produced — after inheritance, after m4 expansion:
$ source path.sh
$ path src | yq -P
- sh: ': ${1:?}; PATH=$(jq "$jq" -nr --args "$@")'
ns: path
id: addp
jq: (env.PATH / ":") as $p | ($ARGS.positional - $p) + $p | join(":")
- sh: ': ${1:?}; PATH=$(jq "$jq" -nr --args "$@")'
ns: path
id: delp
jq: (env.PATH / ":") as $p | $p - $ARGS.positional | join(":")Both addp and delp received the shared sh: body from
inheritance. EP was expanded into the full jq expression. Nothing
is opaque — the transformations are traceable, the output is readable.
This makes baj closer to a templating tool than a traditional
compiler: it generates code you can read, understand, and trust.
- Pipeline details: the four stages in depth (asjs, bm4, dist, emit)
- Authoring guide: writing YAML/Markdown sources, the inheritance model
- Library reference: included libraries (path, git2md, mdq, parsarg, llmfs, baj-ansible)
with-lib: function selection and zero-copy remote execution