Skip to content
MenkeTechnologiesPublic

About

zshrs => zsh Superset

Resources

Stars

17 stars

Watchers

0 watching

Forks

Latest commit

 

History

8,686 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

zshrs

CI Crates.io Downloads Docs.rs Docs License: MIT

[THE FIRST JIT-COMPILED UNIX SHELL]

"No fork, no problems."

The first Unix shell to JIT-compile to native machine code. Bytecode alone is no longer a first: Nushell's IR compiler and evaluator landed in 0.96.0 and became its default evaluator in 0.98.0 — but that IR is interpreted, compiled per parse, and discarded at process exit. zshrs compiles every command (interactive, script, function, sourced file) to fusevm bytecode with fused superinstructions, hands hot blocks to a tiered Cranelift JIT that emits x86-64/aarch64 machine code, and persists the bytecode across processes in rkyv images. No shell before it ran shell source as native code. A drop-in zsh replacement written in Rust, across a 4-crate workspace (zshrs runtime + zshrs-daemon + znative, the published plugin-ABI SDK, + zshrs-runtime, a 16-line crate whose only job is to emit libzsh.a for AOT linking without making cargo install zshrs build it; compsys was folded into the runtime), with the runtime split into a strict 1:1 port directory (src/ported/ — 106 files, every fn maps to a real Src/<x>.c zsh function, enforced by tests/port_purity.rs), a non-port extensions directory (src/extensions/ — features zsh C does not have: AOT, daemon coordination, plugin/script/autoload caches, native fish-ported ZLE engines (syntax highlight, autosuggest, history search, autopair — ON by default in any shell that reads rc files, refused when RCS is unset so bare zshrs -f stays byte-identical to zsh -f; per-feature false in [zle] of ~/.zshrs/zshrs.toml opts out), persistent worker pools, ZWC byte-code helpers), and a feature-gated recorder (src/recorder/). 193 ZLE widgets registered in IWIDGET_NAMES (history navigation, vi find/repeat/marks, undo/redo, isearch, yank-pop, shell-aware word motion, region/visual mode, text objects, completion menu, $zle_highlight parsing), persistent worker pool, AOP intercept, rkyv-backed bytecode images (mmap hot path; the only persisted shell bytecode cache — zsh's .zwc is wordcode for zsh's own interpreter, Nushell's IR never leaves the process), read-only SQLite mirrors beside them for dbview / SQL inspection only (no cache semantics), and full zsh compatibility. Also the first shell to expose its native-plugin interface as a stable, versioned, independently-published ABI — third parties cargo add znative, ship a cdylib, and load it at runtime via zmodload -R (version-gated, mismatches refused). bash enable -f and zsh zmodload load native code too, but only compiled against the shell's private internal headers, welded to one build with no stable ABI; see docs/PLUGINS.md. Also the first shell in the Bourne lineage to ship a language server and a debug adapter in its own binary — zshrs --lsp and zshrs --dap; Nushell (nu --lsp) and Elvish (elvish -lsp) got there first outside that lineage, but bash, zsh, ksh, dash and fish have neither, and their editor tooling (bash-language-server, fish-lsp, vscode-bash-debug) lives in separate Node/TypeScript projects that re-derive a parser the shell already has; zshrs's server answers from the shell's own lexer, parser and live compsys completers. And through its companion build zshrs-native, the first shell with a version control system compiled into it — git served natively as a builtin in the shell's own process, no fork, no exec, no PATH lookup. Since the Bourne shell in 1970 every Unix shell has run git as a foreign binary; BusyBox has no git applet, Nushell's gstat plugin is status-only and runs as a separate child process, and git-shell execs real git. The same build is also the first shell with an fzf-compatible finder compiled in — arb --fzf honors FZF_DEFAULT_OPTS, FZF_DEFAULT_OPTS_FILE and fzf's flag surface in-process, where zsh's key bindings, fzf-tab, fzf.fish and PSFzf all spawn the fzf binary. Elvish is the nearest miss: its histlist and location modes are real in-process fuzzy filtering, but they are shell-internal UI, not a finder that can filter a pipeline.


Table of Contents


[0x00] OVERVIEW

zshrs replaces fork + exec with a persistent worker thread pool, compiles every command to fusevm bytecodes, and persists compiled chunks only in rkyv shards under ~/.zshrs/images/ (single-directory rule — every zshrs file lives under $ZSHRS_HOME / ~/.zshrs/; see docs/DAEMON.md). Beside that tree, catalog.db and related SQL views are read-only mirrors for inspection (dbview, ad-hoc SQL): daemon-hydrated, never authoritative for cache hit/miss or execution. They are not a second shell cache. history.db holds history only — it is unrelated to bytecode caching. The result: shell startup, command dispatch, globbing, completion, and autoloading are all faster by orders of magnitude.

                 [ THE MENKE-TECH REVOLUTIONARY FLYWHEEL ]

                 ┌──────────────────────────────────────┐
                 │       UNIFIED METAMORPHIC FRONT      │
                 │  zshrs  •  8 Bourne Family Dialects  │
                 └──────────────────────────────────────┘
                                    │
                                    ▼
                 ┌──────────────────────────────────────┐
                 │         FUSEVM EXECUTION CORE        │
                 │  Tiered Cranelift JIT / AOT Compiler │
                 └──────────────────────────────────────┘
                                    │
                                    ▼
                 ┌──────────────────────────────────────┐
                 │        SECURE HARDWARE PORTAL        │
                 │ Pure Rust Safety • Zero Memory CVEs  │
                 └──────────────────────────────────────┘

The 8 Bourne-family dialects are the emulation drop-ins --zsh, --bash, --ksh, --mksh, --pdksh, --sh/--posix, --dash, and --ash (the C-shell --csh mode is separate: csh syntax is translated to zsh text by src/extensions/csh/ and run by the same engine, and is checked against tcsh by tests/csh_parity.rs and parity-fuzz --shell csh). All eight compile through the same fusevm core, and each is verified against its real reference shell by the parity matrix in tests/emulation_parity.rs. zsh mode is additionally cross-checked against real zsh by a differential fuzz harness (bins/parity-fuzz.rs) that runs thousands of grammar-driven, seed-replayable snippets through both shells and flags any stdout/exit divergence.


[0x01] INSTALL

# Via Homebrew tap (auto-bumped by each release)
brew tap MenkeTechnologies/menketech
brew install zshrs        # core: zshrs + zd
# OR
brew install zshrs-all    # umbrella: zshrs + zd + zshrs-recorder + zshrs-daemon

# From crates.io
cargo install zshrs
# `zsh` on crates.io is the same source under a second name, published at the
# same version by the same release. `zshrs` is the canonical one.

# From source — lean build, pure shell, no stryke dependency
git clone https://github.com/MenkeTechnologies/zshrs
cd zshrs && cargo build --release
# binary: target/release/zshrs

# Max-perf build (fat LTO, one codegen unit) — what the tagged release
# artifacts and Homebrew bottles are built with. Needs ~8 GB free RAM.
cargo build --profile dist
# binary: target/dist/zshrs

# Set as login shell
sudo sh -c 'echo "$(which zshrs)" >> /etc/shells'
chsh -s "$(which zshrs)"

[0x02] NO-FORK ARCHITECTURE

Every operation that zsh forks for runs in-process. Zero forks for builtins.

Operation zsh zshrs
$(cmd) fork + pipe In-process stdout capture via dup2
<(cmd) / >(cmd) fork + FIFO Worker pool thread + FIFO
cat file fork + exec /bin/cat Builtin — zero fork
head/tail/wc fork + exec Builtin — zero fork
sort/find/uniq fork + exec Builtin — zero fork
date/hostname/uname fork + exec Builtin — direct syscall
sleep/mktemp/touch fork + exec Builtin — zero fork
xattr operations fork + exec xattr Direct syscall — zero fork
pmap/pgrep/peach fork N times VM execution — zero fork
git (via zshrs-native) fork + exec /usr/bin/git Builtin — zvcs linked in, zero fork
fzf (via zshrs-native) fork + exec the fzf binary Builtin — arb's finder linked in, zero fork
**/*.rs Single-threaded opendir Parallel per-subdir descent on the rayon pool ([glob] recursive_parallel, default on)
*(.x) qualifiers N serial stat calls One parallel metadata prefetch
rehash Serial readdir per PATH dir Parallel scan across pool
compinit Synchronous fpath scan Background fpath scan on the worker pool
History write Synchronous fsync Fire-and-forget to pool
Autoload Read file + parse every time Bytecode mmap + zero-copy load from rkyv
Plugin source Parse + execute every startup Delta replay from rkyv image

Coreutils Builtins (Anti-Fork)

48 coreutils commands run in-process with zero fork overhead — the entries of EXT_BUILTIN_NAMES (src/extensions/ext_builtins.rs) that GNU coreutils also installs:

base64  basename  cat  cksum  comm  cut  date  dircolors  dirname  env
expand  expr  factor  fold  groups  head  id  link  logname  mkfifo
mktemp  nice  nl  nproc  paste  printenv  realpath  seq  sha256sum  shuf
sleep  sort  sum  tac  tail  tee  touch  tr  tsort  tty  uname  unexpand
uniq  unlink  users  wc  whoami  yes

Also in-process, outside that set: arch, find, hostname, rev, tput.

Speedup: 2000-5000x per invocation (2-5ms fork overhead → 0.001ms builtin call).


[0x03] BYTECODE COMPILATION

Every command compiles to fusevm bytecodes via a faithful port of zsh's lexer + parser:

Interactive command  ──► lex::zshlex ──► parse::parse ──► ZshCompiler ──► fusevm::Op ──► VM::run()
                         (port of    (port of      (original)
                          Src/lex.c)  Src/parse.c)
Script file (first)  ──► lex::zshlex ──► parse::parse ──► ZshCompiler ──► VM::run() ──► persist rkyv shard
Script file (cached) ──► index.rkyv + mmap shard ──► deserialize Chunk ──► VM::run()
                         (no lex, no parse, no compile)
Autoload function    ──► autoloads.rkyv ──► deserialize Chunk ──► VM::run()
                         (first call in a process compiles the definition
                          file and writes the chunk through; later processes
                          skip lex+parse+compile entirely. Each entry is
                          stamped with the definition file's mtime + length,
                          so an edited function recompiles.)

Measured on _git (424 KB of shell, the largest completer in common use): first git <tab> in a process 1.06 s → 0.56 s once the chunk is cached, and decoding that cached 4.6 MB chunk costs 229 µs against 318 ms to parse + compile the file. The cache is bypassed for ksh_autoload-style bodies and for autoload without -U, where the compiled program is not a function of the file's bytes alone.

The write-through fills one entry per function actually called. To compile the whole corpus up front — so the FIRST ls -<TAB> of a fresh install is already an O(1) shard probe:

zshrs --prewarm-autoloads            # every dir on $fpath
zshrs --prewarm-autoloads DIR ...    # just these
zd prewarm [DIR ...]                 # same, via the daemon

The local daemon's lifecycle is controlled from zd, with no daemon running required:

zd daemon status      # JSON; exit 1 when not running
zd daemon start       # spawn zshrs-daemon detached, wait for the socket; idempotent
zd daemon stop        # returns once the socket is gone; idempotent
zd daemon restart     # stop, then start (new pid)

These act on $ZSHRS_HOME over the local Unix socket (--url does not redirect them). start finds the binary via $ZSHRS_DAEMON_BIN, then beside the running executable, then $PATH.

zshrs-recorder runs the pass at the end of every recording (skip it with --no-prewarm), which is where it belongs: the parser walks process-global lexer state, so this must never run beside a live ZLE. Entries already current are skipped by mtime + length, so a re-run after installing one plugin costs one stat per completer. Budget roughly 6× the source size — a 13k-completer directory compiles to 165 MB in 35 s (debug build).

Enabling the JIT is not the same as being compiled by it. zshrs --tiers script.zsh runs the script and then asks fusevm's own predicates — is_block_eligible, block_jit_is_compiled, trace_is_compiled, find_jit_region — which tier took each chunk, reporting the script body and every function body it dispatched. Chunks that reach neither tier list the op kinds responsible, so the output is a diagnosis (what to make native next) rather than a verdict.

The lexer and parser are direct ports from zsh's C source (Src/lex.c, Src/parse.c); only the bytecode compiler is original Rust. The 4-tier ZshProgram → ZshList → ZshSublist → ZshPipe → ZshCommand AST is preserved verbatim from zsh, ensuring per-construct behavior parity. The bytecode compiler targets the same Op enum that strykelang uses. Both frontends share fused superinstructions, extension dispatch, and the Cranelift JIT path.

Execution Pipeline

┌─────────────────────────────────────────────────────────────────────────┐
│  Script file                                                            │
│       │                                                                 │
│       ▼                                                                 │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │ rkyv bytecode cache (images/*.rkyv + index.rkyv)                 │   │
│  │   lookup(path, mtime) → mmap'd fusevm::Chunk                     │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│       │                                                                 │
│       ├─── HIT (100x faster) ────────────────────────┐                 │
│       │                                               │                 │
│       ▼ MISS                                          ▼                 │
│  lex+parse → ZshCompiler ────────► fusevm::Chunk            │
│                         │                             │                 │
│                         ▼                             │                 │
│                  persist_shard()                      │                 │
│                                                       │                 │
│       ┌───────────────────────────────────────────────┘                │
│       ▼                                                                 │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │                    fusevm::VM::run()                            │   │
│  │                                                                 │   │
│  │  ┌───────────────────────────────────────────────────────────┐ │   │
│  │  │ JIT eligibility check                                     │ │   │
│  │  └───────────────────────────────────────────────────────────┘ │   │
│  │       │                                                         │   │
│  │       ├─── Block JIT (loops, branches) ──► Cranelift ──► x86-64│   │
│  │       │                                                         │   │
│  │       ├─── Linear JIT (straight-line) ──► Cranelift ──► x86-64 │   │
│  │       │                                                         │   │
│  │       ▼ Fallback                                                │   │
│  │  ┌───────────────────────────────────────────────────────────┐ │   │
│  │  │ Interpreter: jump table dispatch + fused superinstructions│ │   │
│  │  └───────────────────────────────────────────────────────────┘ │   │
│  └─────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────┘
Tier What When
rkyv image hit Skip lex/parse/compile Warm script runs
Block JIT Native x86-64 via Cranelift Loops, conditionals
Linear JIT Native x86-64 via Cranelift Straight-line arithmetic
Interpreter Jump table + superinstructions Builtins, I/O, strings

Benchmark: 100x warm start speedup

Cold (cache miss):  717ms  — lex + parse + compile + cache write + execute
Warm (cache hit):     7ms  — deserialize + execute

Cross-shell benchmarks (zshrs vs zsh vs fish vs nushell, bash when present):

bench/run.sh                       # all workloads
bench/run.sh --workload loop       # one workload
ZSHRS_BIN=/path/to/zshrs bench/run.sh

Release binary only (target/release/zshrs); the script refuses to run without it. Workloads (startup, arithmetic loop, function calls, string append, command substitution, external spawn, pipeline, glob) are written once per dialect (POSIX, fish, nushell) so each shell runs its native idiom for the same work. User config is skipped (-f, --no-config, --no-config-file). Output is a hyperfine Markdown table per workload in bench/results.md.

Mean wall time in milliseconds, lower is better. Release build of zshrs 0.13.27; hyperfine --warmup 3 --runs 15; Darwin 25.6.0 arm64; generated 2026-10-10 13:42:51 UTC. Source: bench/results.md.

Workload zshrs zsh fish nu bash ksh mksh dash
Startup (no-op) 7.4 4.6 5.9 11.0 6.5 5.5 2.3 2.3
Arithmetic loop (100000 iterations) 178.4 122.3 1626.5 48.0 150.8 47.5 104.0 150.4
Function calls (20000) 124.8 107.6 353.8 26.8 59.2 16.9 31.0 51.3
String append (5000) 23.8 52.4 200.4 25.6 71.4 11.3 87.4 16.6
Command substitution (500) 90.3 268.9 18.9 707.2 281.6 5.3 209.9 229.1
External command spawn (200 x /usr/bin/true) 226.1 315.4 214.8 276.9 329.2 215.5 297.0 295.3
Pipeline (seq 200000 | sort -n | uniq | wc -l) 57.1 54.7 54.5 58.9 54.4 57.8 54.2 53.0
Glob (src//.rs) 7.0 3.9 3.5 7.7 4.5 3.9 1.9 2.0

[0x04] CONCURRENT PRIMITIVES

Full parallelism in the lean binary. No stryke dependency needed.

# Async/await
id=$(async 'sleep 5; curl https://api.example.com')
result=$(await $id)

# Parallel map — ordered output
pmap 'gzip {}' *.log

# Parallel filter
pgrep 'grep -q TODO {}' **/*.rs

# Parallel for-each — unordered, fire as completed
peach 'convert {} {}.png' *.svg

# Barrier — run all, wait for all
barrier 'npm test' ::: 'cargo test' ::: 'pytest'

pmap / pgrep / peach run their body through the shared fusevm bytecode VM with the {} placeholder substituted as ${=__zshrs_p_arg__} (matching zsh SH_WORD_SPLIT on literal substitution). The body is parsed and compiled to a fusevm Chunk ONCE before the input is iterated; the per-iteration loop sets the param via setsparam, acquires a VM from fusevm::VMPool (preserves Vec capacities across iterations), runs the cached chunk, and releases the VM back to the pool. unsetparam runs once at the end. No fork, no per-iteration parse/compile, no per-iteration VM allocation.


[0x05] AOP INTERCEPT

First shell with aspect-oriented programming:

# Before — log every git command
intercept before git { echo "[$(date)] git $INTERCEPT_ARGS" >> ~/git.log }

# After — timing
intercept after '_*' { echo "$INTERCEPT_NAME took ${INTERCEPT_MS}ms" }

# Around — memoize
intercept around expensive_func {
    local cache=/tmp/cache_${INTERCEPT_ARGS// /_}
    if [[ -f $cache ]]; then cat $cache
    else intercept_proceed | tee $cache; fi
}

The { … } body is a zshrs syntax extension — zsh cannot parse a bare } as an argument. The lexer captures the span between the braces as raw source, so the body keeps its own redirections, pipelines and here-documents, and nothing in it is expanded until the advice fires. zshrs --zsh turns the extension off and rejects the form exactly as /bin/zsh does; intercept before git 'code' works in every mode.

Bound while advice runs: $INTERCEPT_NAME, $INTERCEPT_ARGS, $INTERCEPT_CMD, plus $INTERCEPT_MS, $INTERCEPT_US and $INTERCEPT_STATUS for after.

Advice fires for any command word: an external, a shell function, a builtin (echo, cd, typeset, …), a native command the host binary registers (such as an in-process git) or an alias. An alias is advised under its own name, with $INTERCEPT_ARGS holding the arguments typed after it (not the alias body); intercept_proceed and after advice run the expanded command. An alias named like its expansion (alias ls='ls -G') is advised once, as the expanded command. command NAME and builtin NAME bypass advice, the same way they bypass functions; break and continue compile to jumps and are not advised. The $INTERCEPT_* parameters are unset again once the command has been dispatched. Intercepts are not consulted while advice itself runs, so advice may call the command it advises without recursing; the original command that intercept_proceed runs is an ordinary command again.


[0x06] WORKER THREAD POOL

Persistent pool of [2-18] threads. Configurable:

# ~/.zshrs/zshrs.toml  (single-directory rule; configurable via $ZSHRS_HOME)
[worker_pool]
size = 8

[completion]
bytecode_cache = true

[history]
async_writes = true

[glob]
parallel_threshold = 32
recursive_parallel = true

[zle]                      # fish-ported editor engines, all default on
autosuggest = true
syntax_highlight = true
history_search = true
autopair = true

[provenance]               # value-lineage engine; default true, and inert
enabled = true             # until `provenance -m NAME` arms it
track_all = false          # true = arm every parameter and every shell
                           # function with no `-m` at all

[0x07] RKYV CACHE LAYOUT

Compiled bytecode and plugin/autoload payloads live in rkyv under ~/.zshrs/images/:

Path Purpose
index.rkyv Top-level index: fq_name → shard id, generation, byte offset
images/{hash8}-*.rkyv Mmap-ready shards (system, completions, plugins, scripts, .zshrc, …)
autoloads.rkyv One compiled definition program per autoloaded function, keyed by name and stamped with the resolved fpath directory, a SHA-256 of the exact definition text, and the identity of the zshrs binary that compiled it. Each entry also records what the $fpath search found (the body it read and the state of the files it could have read there), so a later load is served by name with no $fpath search while the directory is still on fpath and those files are unchanged
scripts.rkyv One compiled chunk per script file, keyed by path + mtime
deparse.rkyv Deparsed function bodies (the text $functions prints), keyed by a SHA-256 of the name, the body text and the option state the body is re-lexed under, and stamped with the identity of the zshrs binary that wrote them

SQLite (read-only mirrors) — same directory, different job: daemon-maintained copies you can query with SQL or dbview. They are not the bytecode cache and are not read when deciding cache hit/miss or when running compiled code.

Store Purpose
catalog.db Joinable mirror of catalog metadata (human / tooling reads only)
history.db Command history persistence (orthogonal to bytecode caching — not a cache layer for compiled chunks)
Mirror / FTS views Optional SQL-side views of names and paths for dbview — read-only; see docs/DAEMON.md

Browse mirrors without SQL:

dbview                        # list tables + row counts
dbview autoloads _git         # single function: source, body, bytecode status
dbview comps git              # search completions
dbview history docker         # search history
dbview scripts                 # compiled sourced scripts (scripts.rkyv); optional path substring; CACHED is the last run, hit or store

[0x08] EXCLUSIVE BUILTINS

Parallel Primitives (VM-executed, zero fork)

Builtin Description
async / await Ship work to pool, collect result
pmap Parallel map with ordered output — runs on VM, not fork
pgrep Parallel filter — runs on VM, not fork
peach Parallel for-each, unordered — runs on VM, not fork
barrier Run all commands in parallel, wait for all

AOP / Debugging

Builtin Description
intercept AOP before/after/around advice on any command
intercept_proceed Call original from around advice
doctor Full diagnostic: pool metrics, cache stats, bytecode coverage
dbview Read-only browse of SQLite mirrors; dbview scripts lists the compiled-script rkyv shard entries
profile In-process command profiling with nanosecond accuracy
provenance Value lineage — where a parameter's bytes came from and every bytecode op that touched them, each stamped with file, line and wall clock; shell functions carry the same chain (docs/PROVENANCE.md)
zbanner ZSHRS logo, a box with the version and builtin totals, and a live line: daemon socket status plus this shell's function, alias, parameter and job counts. zshrs --banner prints it from outside a shell, daemon status only

ZSHRS_STARTUP_TRACE=1 turns on the startup phase timer: every startup phase prints [startup <ms since process start>] <label> to stderr, ending with FIRST PROMPT PAINTED, so time-to-first-prompt can be attributed to a phase rather than guessed at. Inert without the variable (one OnceLock<bool> read per mark), so ordinary launches stay silent. Measure the shell itself with ZSHRS_STARTUP_TRACE=1 zshrs -f -i; drop the -f to see what the rc files cost on top of it.

provenance (ported from strykelang's mark / provenance / unmark) answers "where did this value come from?" for a running shell — an origin plus the op chain the bytecode actually executed. No other shell records this: typeset -p answers what a value is, set -x / PS4 answer what ran, and every provenance or taint-tracking system that answers the lineage question does it from outside the shell.

$ cat build.zsh
provenance -m ARCHIVE
REPORT=$(date +%Y-%m-%d)
ARCHIVE=${REPORT}.tar.gz
tar czf $ARCHIVE .
provenance ARCHIVE
$ zshrs build.zsh
ARCHIVE
  origin: cmdsubst "date +%Y-%m-%d" (build.zsh:2, 2026-08-18 11:22:03.908)
  ops:
     1. concat     "2026-08-18" ".tar.gz"                   build.zsh:3              11:22:03.908
     2. assign     ARCHIVE "2026-08-18.tar.gz"              build.zsh:3              11:22:03.908
     3. expand     $ARCHIVE "2026-08-18.tar.gz"             build.zsh:4              11:22:03.909
     4. exec       tar argv[2]                              build.zsh:4              11:22:03.909

Every row carries where and when it happened — file and line, the local clock to the millisecond, and the enclosing shell function when the op ran inside one (at its line in the file the function was defined in).

The chain covers the parameter's whole life, not one value: reassignment appends an op and drops nothing, and a value arriving with its own lineage is spliced in under an origin op.

Shell functions have chains of their own — definition site, every redefinition, every call at the caller's line, and the unfunction that ended it. provenance -m NAME arms whichever the name actually is, so a function needs no extra flag; -f forces the function reading when a parameter of the same name would otherwise win, and reads the chain back:

$ provenance -m greet          # arm it first — nothing is recorded until you do
$ greet; greet
$ provenance greet             # or `provenance -f greet`
greet()
  origin: function greet { MSG="hi $1" } (greet.zsh:2, 2026-08-18 11:48:01.139)
  ops:
     1. call       greet()                                  greet.zsh:3              11:48:01.139
     2. call       greet()                                  greet.zsh:4              11:48:01.140

The origin and every redefine carry the function's body (collapsed to one line, truncated with …), so the chain shows what the body was and what it was changed to. A call op carries the arguments the call was made with — greet world records greet(world) — so repeated calls to one function stay distinguishable; empty or whitespace-bearing arguments are single-quoted and long lists are truncated with ….

Reading a name that was never armed is an error, not an empty chain — provenance -f greet on its own answers not tracked: greet().

Language Model (ai, port of strykelang's ai)

ai calls a language model from inside the shell process. The answer streams to stdout as it arrives, or lands in a parameter with -v/-a — the request and the assignment happen in-process, where reply=$(curl … | jq …) costs two forks, two execs, a pipe and a command substitution.

ai explain the difference between '$@' and '$*'     # streamed to stdout
git diff --cached | ai -v msg -s 'Write a one-line commit message.'
ai -a steps list the steps to rotate a TLS cert     # one array element per line
ai -m claude-haiku-4-5 -t 256 -T 0 summarise: $text
ai -c                                               # cost + token report

With no prompt words, or a lone -, the prompt is read from stdin — so a piped diff is the prompt only when no words follow ai; use -s for the instruction.

Flag Meaning
-m MODEL / -P PROVIDER / -s SYSTEM model id, provider, system prompt
-t N / -T FLOAT / -o SECS max output tokens, temperature, request timeout
-v VAR / -a ARRAY assign to a scalar, or to an array one line per element (mutually exclusive)
-b buffer instead of streaming
-n bypass the response cache
-k mark the system prompt cache_control (Anthropic prompt caching)
-c / -H / -G / -K cost and token report, recent call history, current config, clear the cache
-S key=value change a session default (the [ai] keys below)
-M pattern=response install a mock: a prompt matching the regex answers response without a request

Defaults come from [ai] in ~/.zshrs/zshrs.toml:

[ai]
provider = "anthropic"            # anthropic | openai | local | ollama | gemini
model = "claude-opus-5"
api_key_env = "ANTHROPIC_API_KEY" # the NAME of the variable; the key itself stays in the environment
base_url = ""                     # local/ollama endpoint override
cache = true                      # in-process response cache
max_cost_run = 5.0                # USD ceiling for the shell process; 0 disables
max_tokens = 4096
timeout = 120
Provider Key Endpoint
anthropic (streams) $ANTHROPIC_API_KEY (or whatever api_key_env names) api.anthropic.com
openai $OPENAI_API_KEY api.openai.com
local / compat / openai_compat $ZSHRS_AI_LOCAL_KEY base_url, else $ZSHRS_AI_BASE_URL, else localhost:1234
ollama — base_url, else $OLLAMA_HOST, else localhost:11434
gemini / google $GOOGLE_API_KEY or $GEMINI_API_KEY generativelanguage.googleapis.com

Every call is billed against max_cost_run, from a price table built into the binary; once the process has spent that much, ai refuses rather than calling. The buffered Anthropic path retries 429, 500, 502, 503 and 504 up to four attempts with exponential backoff. ZSHRS_AI_MODE=mock-only makes a prompt that no -M mock matches an error instead of a live call — what a test suite wants. Exit status: 0 on success, 1 when the call failed (no key, HTTP or transport error, cost ceiling), 2 for bad flags; the reason goes to stderr as zshrs: ai: <reason>.

Nothing is recorded until provenance -m arms it — or until something turns on track-everything mode, which arms every parameter write and every function with no -m at all: provenance -a at runtime, [provenance] track_all = true in ~/.zshrs/zshrs.toml from startup, or ZSHRS_PROVENANCE_ALL=1 in the environment. Self-rewriting parameters (LINENO, RANDOM, status, the positionals, …) stay out of it, and 4096 auto-armed names is the ceiling. [provenance] enabled = false (or ZSHRS_PROVENANCE=0) refuses arming altogether.

Unit Test Framework (port of strykelang)

Builtin Description
zassert_eq / zassert_ne Equality / inequality
zassert_ok / zassert_err / zassert_true / zassert_false Truthiness
zassert_gt / zassert_lt / zassert_ge / zassert_le Numeric ordering
zassert_match Regex match
zassert_contains Substring containment
zassert_near Float approximate equality (epsilon)
zassert_dies Passes when given shell command exits non-zero
ztest_skip Mark current assertion skipped
ztest_run / run_tests Print summary, roll counters into totals
zshrs --ztest [paths] Worker-pool runner — fork-on-receive (one persistent worker per CPU, dispatches test_* / t_* files under t/ or tests/)
zshrs --ztest-worker Persistent worker subprocess (JSON over stdin/stdout)

Coreutils (Anti-Fork)

Builtin Description
cat Concatenate files — no fork
head / tail First/last N lines — no fork
wc Line/word/char count — no fork
sort / uniq Sort and dedupe — no fork
find Walk directories — no fork
cut / tr / rev Text manipulation — no fork
seq Number sequences — no fork
tee Copy stdin to files — no fork
date Current date/time — direct syscall
sleep Delay — no fork
mktemp Create temp file/dir — no fork
hostname / uname / id / whoami System info — direct syscall
touch / realpath / basename / dirname File ops — no fork
zgetattr / zsetattr / zdelattr / zlistattr xattr ops — direct syscall

[0x09] SHELL LANGUAGE FEATURES

Every shell construct compiles to fusevm bytecode — no tree-walker dispatch lives in zshrs. The full reference documents each entry with a runnable example.

Control flow

# Standard POSIX/zsh control structures — all compile to fusevm bytecode
if [[ -d $dir ]]; then …; elif [[ -f $dir ]]; then …; else …; fi
while (( i < 10 )); do …; done
until ping -c1 host >/dev/null; do sleep 1; done
for f in *.rs; do echo "$f"; done
for ((i=0; i<10; i++)); do …; done
case $cmd in start) … ;; stop) … ;; *) … ;; esac
select choice in build test deploy; do … done   # interactive numbered menu
coproc { while read l; do echo "ECHO: $l"; done } # bidirectional pipe

Indexed arrays

arr=(alpha beta gamma)            # literal
arr+=(delta epsilon)              # append
echo ${arr[1]}                    # alpha (1-based)
echo ${arr[-1]}                   # epsilon (negative from end)
echo ${arr[@]}                    # splice — N argv slots
echo ${#arr[@]}                   # length
for x in ${arr[@]}; do …; done    # iterate (flattens via BUILTIN_ARRAY_FLATTEN)

Associative arrays

typeset -A m                      # declare
m[name]=Alice; m[role]=eng        # set
echo "${m[name]}"                 # lookup
for k in "${(k)m}"; do echo $k; done   # keys
for v in "${(v)m}"; do echo $v; done   # values

Parameter expansion flags (zsh-style)

echo ${(L)var}                    # lowercase
echo ${(U)var}                    # uppercase
echo ${(j: :)arr}                 # join with space
echo ${(s:,:)scalar}              # split on comma → array
echo ${(f)$(cmd)}                 # split on newlines
echo ${(o)arr}                    # sort ascending
echo ${(O)arr}                    # sort descending
echo ${(P)ref}                    # indirect lookup
echo ${(jL)arr}                   # stack: join then lowercase
echo ${(s:,:U)scalar}             # stack: split then uppercase

Parameter expansion forms

${var:-default}                   # default if unset/empty
${var:=default}                   # assign default
${var:?msg}                       # error if unset
${var:+alt}                       # alternate if set
${#var}                           # length
${var:offset:length}              # substring
${var#pat} / ${var##pat}          # strip shortest/longest prefix
${var%pat} / ${var%%pat}          # strip shortest/longest suffix
${var/pat/repl} / ${var//pat/repl} # replace first/all
${var:u} / ${var:l}               # upper/lower case (zsh postfix)

Background, async, coprocesses

sleep 30 &                        # fork + setsid; parent gets Status(0)
jobs; fg %1; wait $!              # job control
async 'expensive-task' | xargs await   # worker-pool, no fork
coproc { body }                   # bidirectional pipe; $COPROC=[rd_fd, wr_fd]
echo hi >/dev/fd/${COPROC[2]}     # write to coproc stdin
read line </dev/fd/${COPROC[1]}   # read from coproc stdout

Eval, dynamic dispatch, AOP

eval 'echo $x'                    # single-quoted args defer expansion correctly
cmd=ls; $cmd -la                  # dynamic command name routes through host intercepts
intercept before git { …; }       # AOP advice fires for both literal and dynamic invocations

Runnable demos

examples/demos/*.zsh — 375 self-contained scripts, every one pinned to the same zshrs binary that runs CI. Sixteen batches:

Range Theme
01–30 shell fundamentals (arithmetic, arrays, assoc, control flow, fn/recursion, brace + parameter expansion, parameter flags, heredocs, command/process subst, pipes, printf, traps, IFS, anon fn, positional args, typeset, pattern match)
31–60 data structures (stack, queue, set ops), sorting (bubble/insertion/selection/counting), search (binary), classic algorithms (matrix mul, roman, hanoi, collatz, happy, armstrong, perfect, rot13, atbash, GCD/LCM), zsh idioms (CSV, env, file tests, date, read loop, exit codes, atoi, mapfile)
61–85 zsh C-feature demos — each cites Src/*.c: :h:t:r:e:a:A:s modifiers (Src/hist.c), parameter flags (M)(P)(Q)(V)(j)(s)(o)(O)(u)(L)(U)(C)(l)(r) (Src/subst.c paramsubst), glob qualifiers (.)(/)(@)(N)(om)(oL) (Src/glob.c), extended glob ^pat ~ # ## (alt|alt) **/* (Src/pattern.c), associative-array advanced ops (Src/params.c PM_HASHED), array set ops :|/:*, pattern-filter ${arr:#pat} / (M):#, typeset -i base, print -aC columnar (Src/builtin.c bin_print), print -P prompt escapes (Src/prompt.c), zparseopts (Src/Modules/zutil.c), zsh/mathfunc (Src/Modules/mathfunc.c), zsh/datetime (Src/Modules/datetime.c), setopt local_options, eval + dynamic dispatch (Src/builtin.c bin_eval), anonymous fns (Src/exec.c is_anonymous_function_name), compound defaults, advanced brace expansion (Src/glob.c bracecomplete), history-style word modifiers, complex split/join, mini calc REPL
86–110 advanced runtime patterns — setopt exhaustive (Src/options.c), read -A -d (Src/builtin.c bin_read), printf format dispatch, [[ … =~ … ]] + $match (Src/cond.c cond_match + Src/Modules/regex.c), type/whence/which (Src/builtin.c bin_whence), hash cmd cache (Src/builtin.c bin_hash), C-style for with multi-counter (Src/parse.c), 2D-assoc emulation, case alternation + fallthrough ;& (Src/exec.c execcase), fd redirection + &> + tee (Src/exec.c addfd), strict mode set -euo pipefail, variable indirection (P) + eval, coreutils builtins (anti-fork extension), negative+ranged indexing, zsh-features summary, subshell-vs-group scope, function introspection (functions[] assoc), EXIT/ERR/ZERR traps, arithmetic edge cases, dispatch table via assoc, 3-stage pipelines + pipefail, eval metaprogramming, globsubst/=cmd/nullglob, boolean truth tables, getopts+until+repeat+time+break N
111–135 extension + utility patterns — let builtin (Src/builtin.c bin_let), assignment forms (Src/exec.c addvars), typeset -T tied colon-arrays (Src/params.c PM_TIED), local -x -g -i -r -a -A modifiers, greedy ##/%% strips (Src/subst.c), conditional numeric ops -eq/-gt (Src/cond.c cond_val), capture-aware replacement, recursive **/ globs (Src/glob.c), background & + wait (Src/jobs.c), UTF-8 string handling, mini-cat/mini-grep/mini-wc (pure-zsh coreutils), URL encode/decode, JSON pretty-printer, XML entity escape, string trim/pad/center, CSV writer with proper quoting, assoc serialize/deserialize, INI file parser, emulate -L sh|ksh (Src/options.c bin_emulate), ksh-style @()/+()/!() patterns (Src/pattern.c), zstyle context store (Src/Modules/zutil.c bin_zstyle), compdef completion signatures (Src/Zle/compsys.c), bindkey keymap API (Src/Zle/zle_keymap.c)
136–160 systems + algorithms + apps — path[] tied to $PATH (Src/params.c PM_TIED), named pipes via mkfifo, file-lock via mkdir-atomic, env manipulation deep, kill -USR1/USR2 signal handling (Src/signals.c), ANSI 256-color + print -P %F, calculator engine over $((…)), todo-list CRUD, BFS over adjacency-list graph, finite state machine via transition map, topological sort over DAG, pomodoro timer w/ EPOCHREALTIME, inventory system, append-only event log with filter/aggregate, LRU cache w/ eviction, sorted-output priority queue, Bloom filter w/ 3-hash, trie (prefix tree), Levenshtein edit distance (DP), line-level + word-level diff, {{var}} template renderer, observer pattern w/ subscriber registry, deterministic $RANDOM (Src/params.c randomgetfn / seed + Fisher-Yates shuffle), bank-account ledger w/ journal, self-introspecting capabilities report
161–185 utilities + meta — pushd/popd/dirs directory stack (Src/builtin.c bin_pushd), umask + ulimit (Src/builtin.c bin_umask/bin_ulimit), ${(q)} / ${(qq)} / ${(qqq)} / ${(qqqq)} quoting flags (Src/subst.c), ${(z)} shell-words split (Src/lex.c), print -aC -N -f -P advanced flags (Src/builtin.c bin_print), (#i)/(#a)/(#m) pattern flags (Src/pattern.c), pure-zsh mini_find w/ -type/-name predicates, mini Makefile-style build w/ dep DAG, Markdown→plain-text stripper, regex tester driver, moving avg + peak detection + cumulative sum + trend, password strength scoring, anagram finder via canonical-form grouping, integer-to-English-words (0..999999), ASCII charts (horizontal + vertical + histogram), Conway's Game of Life (4 generations), days-between calculator w/ leap-year + Zeller's congruence, DFS maze generator w/ seeded $RANDOM, lottery simulator w/ match histogram, multi-month calendar printer, 6 FizzBuzz styles + reverse-FizzBuzz + counts-in-1..100, progress bars + braille spinners, word-frequency w/ top-N + once-only + filtering, cron-expression matcher (* */N 0), final 185-demo recap
186–210 parsers + apps + meta — alias/-g/-s forms (Src/builtin.c bin_alias), xxd-style hex dump, IPv4 parser + validator + subnet math, HTTP status code lookup + classifier, ANSI escape stripper, retry-with-exponential-backoff, memoize wrapper w/ stats, log rotation w/ size threshold, URL parser (scheme/user/host/port/path/query/fragment), 4 toy hash functions (poly + djb2 + FNV-1a + Adler-32), Base64 encoder w/ url-safe variant, full RFC4180 CSV parser w/ quoted fields + escaped quotes + embedded newlines, naive YAML key:value parser, 256-color + truecolor palettes, milestone banner (200th demo), unit converter (length/weight/temp/time), expression tokenizer, subcommand dispatcher CLI, string-interpolation patterns, zsh-specific scripting idioms, advanced assoc-array iteration (sort/filter/partition/transform), TTL cache w/ GC, recursive directory walker w/ emoji + size + extension stats, map/filter/reduce composable pipeline, quine + reflective ${functions[name]} + mutual recursion
211–235 apps + games + introspection — CSV→Markdown table, Markdown table renderer w/ alignment, SSH config parser (Host blocks), chess board renderer from FEN, Tic-Tac-Toe (3 scripted games + win detect), card deck (Fisher-Yates shuffle + poker classify), number-guess (binary-search vs random over 100 games), quiz game w/ scoring + grades, Mad-libs template engine, expense tracker w/ category totals + percentages, set -x xtrace + $PS4 customization (Src/builtin.c bin_set), $PS1/$PS2/$PS3/$PS4/$RPROMPT config + expansion (Src/prompt.c), $funcstack/$functrace call-stack introspection (Src/exec.c), git-log parser (commit/author/date/subject), Nginx-log analyzer (status + IP + method + bytes), todo w/ categories + priorities + due dates, open-addressing hash table impl, RPN stack machine (ADD/MUL/DUP/SWAP/DROP), multi-field array search + filter + group-by, menu-driven app w/ sub-menus, text adventure (rooms + exits + path), time tracker w/ project totals + bar chart, word-chain game (last-letter→first-letter), persistent KV-store mock (load/save assoc), grand-finale 235-demo banner
236–260 hooks + cryptography + grids + parsers — precmd/preexec/chpwd hooks via add-zsh-hook (Src/Functions/Misc/add-zsh-hook), autoload -Uz from fpath (Src/builtin.c bin_functions), Dijkstra shortest-path on weighted graph A-F, Sudoku validator (rows/cols/3x3 blocks), Lights Out 5×5 puzzle w/ toggle propagation, Hangman game w/ 7-stage ASCII gallows, famous number sequences (Fibonacci/Catalan/Lucas/Bell/triangular/pentagonal/hexagonal/factorial), Sierpinski triangle (Pascal mod 2 + bitwise + carpet), Mandelbrot set ASCII (fixed-point math, 60×24), TOML parser (sections + key/value + dotted lookup), .env-file parser w/ quoted + comments + export prefix, shebang detector (env-style + direct-path classification), charset validator (ASCII/hex/b64/UUID/email/url/ident), whitespace normalizer (strip/collapse/expand/unexpand/EOL/squash), shopping cart w/ tax + discount + inventory check, Vigenère cipher (encrypt/decrypt/identity/tableau), Caesar cipher + ROT13 + brute-force, word-search 8-dir solver w/ flat-string grid, ASCII 7-segment digital clock, IPv6 parser (expand :: + compress longest zero-run), recipe-unit converter (fractions + scaling + vol/mass), memory-match concentration grid, monoalphabetic substitution cipher w/ keyed alphabet + frequency analysis, Boggle 4×4 DFS solver w/ 52-word dict, final 260-demo banner v3
261–285 crypto + graphs + games + zsh hooks — prime factorization (trial division + canonical form), Miller-Rabin probabilistic primality w/ deterministic witnesses, extended Euclidean + modular inverse + RSA-toy, A* pathfinding on ASCII grid w/ Manhattan heuristic, Kruskal's MST w/ union-find + path compression, Prim's MST grown from start, Floyd-Warshall all-pairs shortest paths, Bellman-Ford + negative-cycle detect, N-Queens count + render (n=1..7 vs OEIS A000170), 15-puzzle slide + inversion-count solvability, Towers of Hanoi w/ animated render + 2ⁿ-1 verification, Markdown→HTML single-pass tokenizer (headings/bold/italic/code/lists/links/fenced), HTTP request parser (method/path/query/headers/body/cookies), log format auto-detector (Apache CLF/JSON/syslog/nginx/logfmt), CSV inner+outer join + aggregation, Blackjack dealer-17 + hand-value w/ Ace soft/hard, dice probability + Yahtzee pattern classifier + χ² fairness, Rock-Paper-Scissors 6-strategy round-robin tournament, XOR cipher + frequency analysis + Hamming key-length probe, One-Time Pad w/ key-reuse weakness demo, periodic/precmd/preexec/chpwd hooks (Src/init.c periodic_sched_cmd + add-zsh-hook), positional params + getopts deep dive (Src/params.c $argv + Src/builtin.c bin_getopts), trap matrix (EXIT/ERR/ZERR/USR1/USR2/INT/TERM/HUP via Src/signals.c install_handler), atomic file write via tmp+rename + mkdir-lock + toy CAS, grand finale 285-demo banner v4
286–310 trees + games + strings + zsh internals — segment tree (range sum + point update + 200-query stress), Fenwick BIT tree (prefix sum + inversion count via O(n log n)), KMP string matching (failure table + period detect + naive cross-check), Rabin-Karp rolling-hash w/ collision verification, Manacher's longest palindrome O(n) + palindromic-substring count, reservoir sampling Algorithm R (Fisher-Yates cross-check), probabilistic skip list (level distribution + contains), suffix array + Kasai LCP (longest repeated substring + unique-substring count + binary-search lookup), word ladder BFS over 80-word dict (cat→dog chains), Soundex phonetic hash (Robert/Ashcraft/Tymczak classics), Minesweeper flood reveal w/ adjacency histogram, Mastermind w/ black/white peg scoring + color-frequency analysis, Tic-tac-toe minimax (terminal-state scoring), Conway's Life animated multi-pattern (glider/blinker/block), 🎉 demo 300 milestone banner, URL template (RFC 6570: {?q}/{+base}/{#anchor}/{/path}), mini SQL SELECT parser + in-memory executor (WHERE/ORDER BY/LIMIT), print -P/-v/-aC/-z/-l/-N flags (Src/builtin.c bin_print), unalias/unset/unhash/unfunction w/ -m pattern flag (Src/builtin.c bin_unhash), typeset -m/-i/-F/-T/-A deep dive (Src/builtin.c bin_typeset), zle widget definitions + bindkey + keymaps + BUFFER/CURSOR (Src/Zle/zle_main.c), compdef + _arguments + zstyle contexts (Src/Zle/compsys.c), graph density study (Kruskal MST cost vs density% trials), finite state machine DSL (turnstile/traffic light/vending/TCP), grand finale 310-demo banner v5
311–335 trees + DP + zsh deep dives — iterative BST (insert/contains/inorder via stack), AVL rotation logic (LL/RR/LR/RL cases), Bloom filter v2 + union/intersection, double-ended queue (sliding-window max + BFS), ring buffer w/ overwrite, IPv4 subnet calc (CIDR/mask/broadcast/contains), MAC address parser + OUI vendor DB, file checksums (Adler-32/DJB2/FNV-1a/sum16/XOR), anagram solver (canonical sort + grouping + phrases), leet speak basic + advanced + random, pig latin encode + reverse, zsh HISTFILE parser (extended : ts:dur;cmd + raw + freq stats), SSH known_hosts parser (plain + hashed + duplicate detect), brace expansion deep dive (numeric/alpha/nested/product/padded), print -r/-R/-D/-aC/-P/-v/-u/-s/-z/-N/-m/-o/-O/-i/-e/-E flags (Src/builtin.c bin_print), compinit + completion lifecycle (Src/Zle/compsys.c), extended_glob deep dive (^/~/#/##/<a-b>/(#i)/(#a) + qualifiers, Src/pattern.c), assoc array (@kv) flag + 2-dim + invert + merge (Src/params.c PM_HASHED + Src/subst.c paramsubst), max subarray (Kadane + circular + stock profit), LIS + LCS + edit distance DP, 0/1 knapsack + fractional comparison, coin change (min + count ways + reconstruction), topological sort (Kahn's + cycle detection w/ partial-order recovery), LRU cache (doubly-linked list + hash + working-set sim), grand finale 335-demo banner v6
336–360 history math + parsers + ciphers + zsh metadata — Roman numeral encoder/decoder + validator, trie (insert/search/autocomplete/count_prefix/longest_common_prefix), Z-function + Z-based substring search + period detection, longest common substring DP + brute-force cross-check, longest palindromic subsequence DP + min-insertions, Nim game + XOR theorem + optimal-play strategy proof, peg solitaire + greedy solver, RFC 2822 date parser (email-header format) + epoch conversion, ISO 8601 parser (date/time/duration/week/ordinal), Pollard's rho factorization + Miller-Rabin small primality, continued fractions + sqrt approximation + golden ratio + Lagrange theorem, columnar transposition cipher + rail fence, color conversions (RGB↔HSL↔HSV + hex parse + 256-color mapping + luminance + WCAG contrast), $ZSH_EVAL_CONTEXT + $FUNCNEST + $ZSH_SUBSHELL (Src/init.c eval_context + Src/exec.c subshell counter), 🎉 demo-350 milestone banner, Sokoban small puzzle + push/wall semantics, text wrap (greedy + center + right + full-justify) + whitespace normalization, Unicode utilities (display width + byte count + JSON escape + hex escape), URL encode/decode RFC 3986 + URL parser + query string + form encoding, calendar (month grid + year overview + Zeller's congruence + leap year), disjoint-set union (path compression + grid islands + friendship circles), priority queue (min-heap + Dijkstra application + top-N stream), $funcstack/$funcfiletrace/$functrace/$funcsourcetrace (Src/exec.c func stack), comprehensive parameter expansion flags ((U)/(L)/(C)/(j)/(s)/(o)/(O)/(q)/(qq)/(qqq)/(qqqq)/(M)/(P) exhaustively, Src/subst.c paramsubst), grand finale 360-demo banner v7
361–367 substantial functional demos (500+ LOC each) — comprehensive JSON parser (RFC 7159: tokenizer + AST + JSONPath + pretty-print), full XML parser (tags + attributes + CDATA + comments + entity decode + XPath subset), arithmetic expression evaluator (Shunting-yard tokenizer + RPN stack-based eval + variables + functions abs/min/max/sqrt), RFC 4180 CSV parser (state machine + quoted fields + embedded newlines + CRLF + custom delimiter + round-trip serializer), mini-Lisp interpreter (tokenizer + s-expression parser + closures + recursion + cond/let/lambda/define + lexical scope chain), Sudoku backtracking solver (row/col/3×3 block validation + algorithm reference), grand finale 367-demo banner v8
368–375 codecs + interpreters + algorithms — Bencode encoder/decoder (BitTorrent wire format), Hamming(7,4) single-bit error-correcting block code, skyline problem (key-point outline from N building triples), Brainfuck interpreter (pure-zsh), Ackermann function (total non-primitive-recursive, Ackermann 1928 / Péter simplification), LZW (Lempel–Ziv–Welch) compression (the algorithm behind GIF + Unix compress), Elias gamma universal codes for positive integers, grand finale 375-demo banner v9
cargo build --bin zshrs
target/debug/zshrs --zsh examples/demos/10_fizzbuzz.zsh
cargo test --test examples_demos_ci          # full sweep, ~46s parallel

[0x0A] COMPATIBILITY

  • Full zsh script compatibility — runs existing .zshrc
  • Full bash compatibility via emulation
  • Native fish-ported line-editor engines (ON by default; every engine is refused when RCS is unset, so bare zshrs -f stays byte-identical to zsh -f for parity, and per-feature false in ~/.zshrs/zshrs.toml [zle] opts out): syntax highlighting (lexer-driven command/keyword/quote/redirection/path coloring with command-validity and file-existence checks; the engine is the fish port, the palette and word-level classification are fast-syntax-highlighting's — command-type ladder, brackets-by-nesting-level pass, glob/variable/path/option/math/here-string/case styles, secondary theme inside $(…), and the autoload / source / printf chromas — so the native pass renders the same as the plugin it replaces), history autosuggestions (ghost text, accept with →/End, word-wise accept with forward-word), prefix/substring up-arrow history search, and bracket/quote auto-pairing (zsh-autopair port) — ports of the fish-shell Rust engines that zsh-syntax-highlighting, zsh-autosuggestions, and zsh-history-substring-search recreate in script. Existing plugin config applies (ZSH_HIGHLIGHT_STYLES, ZSH_AUTOSUGGEST_*, HISTORY_SUBSTRING_SEARCH_*, AUTOPAIR_*); the native engines stay authoritative even with the script plugins loaded (their config honored, their widgets subsumed); ZSHRS_NATIVE_ZLE_FX=0 force-disables. Per-keystroke passes are wall-clock budgeted (ZSHRS_ZLE_{HIGHLIGHT,AUTOSUGGEST}_BUDGET_MS, default 8) so huge directories or PATHs can never lag typing
  • Fish-style abbreviations
  • Builtins: zsh's own ports plus zshrs extensions (the latter including 48 coreutils and 6 parallel/async primitives) — see the Reference for the full catalog
  • Self-contained fpath — zsh's whole function tree (Completion/** + Functions/**, flattened the way make install flattens it) is packed into the binary at build time and written to ~/.zshrs/functions on first run, alongside zshrs's own per-builtin completions. That directory replaces the host zsh tree at that tree's index in an inherited FPATH, and sits last otherwise, so directories a config prepends out-rank it. The bundle is placed when the startup fpath is built (from the default or an inherited FPATH); a later fpath=( … ) or fpath+=( … ) stores exactly what was assigned, as in zsh. Host zsh installation directories are dropped — from the compiled-in default and from an inherited FPATH alike — so <prefix>/share/zsh/<version>/functions never shadows the bundle; share/zsh/site-functions (third-party completions) is kept. zshrs's compinit, _git and _describe are the ones it shipped with, and a Homebrew zsh upgrade cannot change how the shell completes. User and plugin directories are untouched. The tree is re-written when the bundle's content hash changes, not merely when the version does
  • async_precmd hook — precmd-style functions registered here run on a pool worker thread instead of blocking the prompt, so a slow vcs status or network check never delays the prompt paint. The batch is serialized against the shell thread's own shell code — parameter scopes are one counter and one table, shared — so a widget, hook or command waits for a batch that is already running, and withdraws one the pool has not started yet (its hooks run at the next prompt). Plain typing never waits: builtin widgets open no scope. Registered the normal way, add-zsh-hook async_precmd my_fn: zshrs ships an add-zsh-hook whose hook list knows the name, since upstream's fixed list rejects it
  • Bundled man / info — zsh's manual pages (zsh, zshall, zshbuiltins, zshexpn, zshmisc, zshoptions, zshparam, zshzle, zshcompsys …) and the Texinfo manual ship in the binary the same way, materialising to ~/.zshrs/man/man1 and ~/.zshrs/info. Those directories are prepended to MANPATH and INFOPATH with an empty trailing entry, so the system's own search path is still spliced in and every other page on the machine keeps resolving. man zshall and info zsh work on a host with no zsh installed. zshrs's own pages (zshrs, zshrsall) ship beside them, and the bundled zshall .so-includes zshrsall, so man zshall documents the zshrs superset (zshrs-only builtins, daemon, AOT, intercepts, parity modes) after the zsh sections. run-help's help database and the newuser script ship the same way, to ~/.zshrs/help and ~/.zshrs/scripts, with HELPDIR pointed at the former unless the user set one — zsh's own run-help hardcodes the help path of whichever zsh built it, which is dead on any other machine. HELPDIR is a shell parameter only, never exported: run-help is a shell function that reads it out of the parameter table, so there is nothing to put in a child's environment. Under --zsh none of this is published (no MANPATH/INFOPATH entries, no HELPDIR), matching zsh.
  • ZWC precompiled function support
  • Glob qualifiers, parameter expansion flags, completion system
  • zstyle, ZLE widgets, hooks, modules
  • Per-shell emulation drop-ins: --zsh, --bash, --ksh, --mksh, --pdksh, --sh/--posix, --dash, and --ash. --mksh (MirBSD ksh) and --pdksh (Public Domain ksh) share the --ksh base; --ash (Almquist shell) is an alias of --dash. --dash is a strict Debian Almquist Shell mode — it applies the sh option presets and additionally rejects the zsh-only syntax dash has never had ($'...' ANSI-C quoting, <<< here-strings, += compound assignment, name=(...) arrays, the [[ ]] reserved word, arith **/,, and printf %q) while using XSI echo — verified byte-for-byte against /bin/dash
  • Startup files follow the emulated shell, not zsh, and every drop-in works as a real login shell (argv[0]-dash install, per-shell profile/rc/logout chain, $ENV / $BASH_ENV, bash's --norc / --noprofile / --rcfile, and bash's $PS1 backslash escapes with ${var@P}). Each row is verified against the reference binary — bash 5.3.15, ksh93, mksh, dash, tcsh, zsh 5.9 — not against its manual. See the login-shell coverage matrix below.
  • Drop-in fidelity beyond zsh's own emulate: the bare POSIX-family modes match the REAL shell, not zsh's approximation of it. --sh/--dash/--ash use XSI echo (backslash escapes interpreted without -e, as both /bin/sh flavours and dash do) where zsh's emulate sh sets BSD_ECHO, and --bash accepts bash's own set -o names — including the six zsh has no option for (posix, errtrace, functrace, history, keyword, nolog) — reporting them through set -o, set +o and $SHELLOPTS. --bash also fires trap … RETURN when the installing function returns, prints hash and enable listings in bash's formats, lays declare -f compound bodies out as bash's print_cmd does, fills BASH_SOURCE / BASH_LINENO / FUNCNAME (with main) for caller, supports wait -n, mapfile -d '' / -O, and the set -B/-h/-E/-H/-P/-T letters with bash's $-, and expands braces before parameters (x=; echo $x{,a} prints an empty line). The same holds for the per-shell quirks zsh's emulate lacks: --dash rejects special-builtin function names, aborts on exit abc / return abc, treats a bare read as arg count, ignores arguments to ., and keeps a leading // in cd paths; --ksh decodes print / echo -e / printf %b escapes with ksh93's narrower set, accepts blanks in an assignment subscript (h[a b]=1) and scopes an EXIT trap to a function name { … } body; --ksh also expands stepped character ranges ({a..e..2}), rounds printf %f ties away from zero, gives name() { typeset x; } no local scope and keeps a[1+1]=x sparse; --mksh lists typeset -p in mksh's attribute order and supports ${v@Q}; --mksh aborts on a trap usage error; and cd - / ~- read $OLDPWD in every drop-in but the internal oldpwd in zsh. Adding --zsh (--sh --zsh, --ksh --zsh) selects zsh-STYLE emulation instead: the option deltas a real zsh installs for emulate sh / emulate ksh, referenced against zsh itself.

Login-shell coverage matrix

Every drop-in is usable as a real login shell — installed by symlinking the binary under the target shell's name and letting login(1) / sshd exec it with a leading dash on argv[0]:

ln -s /path/to/zshrs ~/bin/bash    # the NAME selects the personality
# add the path to /etc/shells, then
chsh -s ~/bin/bash

The symlink's NAME is what selects the personality — the same argv[0] inference C zsh does at Src/init.c:1869+ — so install one link per shell you want to stand in for. Each row names the binary its behaviour was measured against:

drop-in symlink name login shell starts it as verified against
--bash bash -bash GNU bash 5.3.15
--ksh ksh -ksh ksh93 (AT&T; macOS /bin/ksh)
--mksh mksh -mksh mksh (MirBSD Korn shell)
--pdksh pdksh -pdksh not installed here — rules taken from OpenBSD ksh(1)
--sh / --posix sh -sh /bin/sh (bash 3.2 in sh mode on macOS, dash on Debian)
--dash / --ash dash / ash -dash / -ash dash
--csh csh -csh tcsh (macOS /bin/csh)
--zsh zsh -zsh zsh 5.9

The whole table below is re-checked against those binaries by the startup-file parity + fuzz harness on every run. $ENV is expanded (parameter, command, arithmetic and tilde substitution) before use in every shell that reads it.

drop-in login shell reads interactive non-login reads non-interactive reads on login exit
--bash /etc/profile, then the first readable of ~/.bash_profile, ~/.bash_login, ~/.profile /etc/bash.bashrc (where shipped), ~/.bashrc $BASH_ENV ~/.bash_logout
--ksh /etc/profile, ~/.profile, then the interactive file /etc/ksh.kshrc (where shipped), $ENV — default ~/.kshrc — —
--mksh /etc/profile, ~/.profile, then the interactive file $ENV, or ~/.mkshrc when $ENV is unset — —
--pdksh /etc/profile, ~/.profile, then the interactive file $ENV — no default — —
--sh / --posix /etc/profile, ~/.profile, then the interactive file $ENV — no default — —
--dash / --ash /etc/profile, ~/.profile, then the interactive file $ENV — no default — —
--csh the non-login files, then /etc/csh.login, ~/.login /etc/csh.cshrc, then ~/.tcshrc or ~/.cshrc same as interactive — csh reads its rc file either way ~/.logout, /etc/csh.logout
--zsh /etc/zprofile, ~/.zprofile, then the interactive files /etc/zshrc, ~/.zshrc /etc/zshenv, ~/.zshenv (read on every invocation) ~/.zlogout, /etc/zlogout

Rules that differ per shell, each measured rather than assumed:

behaviour bash ksh / mksh / pdksh / sh / dash csh zsh
interactive login shell also reads the interactive rc file no — profile only yes yes yes
login by argv[0] dash alone, non-interactive, reads the profile no — needs -i or an explicit --login yes yes yes
-f means noglob noglob no-rcs no-rcs
suppress every startup file --norc --noprofile, or --no-rcs --no-rcs --no-rcs -f / --no-rcs
$PS1 escapes backslash (\u, \h, \w, \$, …), plus ${var@P} literal text literal text % escapes
bracketed paste (\e[?2004h) yes no no yes
OSC 133 shell-integration marker no no no yes
zsh partial-line % marker (PROMPT_SP) no no no yes
zshrs's own line-editor engines (autosuggestion ghost text, input syntax highlighting, history search, autopair) off off off on

Startup state beyond the files — also per-shell, also measured:

The Korn column targets the maintained ksh93u+m 1.0.10 line, not AT&T ksh93u+ 2012 (which macOS still ships as /bin/ksh). The two differ in both rows below: the 2012 line shipped 19 default aliases and printed a labelled user\t0m0.00s for times, and 93u+m ships neither. Following the maintained fork also fails safer — omitting an alias leaves a command resolving to its builtin, whereas inventing one shadows a real command.

behaviour bash ksh93 mksh / pdksh dash / ash / sh zsh
default aliases none none 11 (autoload, functions, r, …) none 2 (run-help, which-command)
alias listing form alias NAME='value' NAME=value NAME=value NAME=value NAME=value
export -p / readonly -p declare -x NAME="v" / declare -r export NAME=v export NAME=v export NAME='v' zsh's own
times fraction milliseconds milliseconds, seconds zero-padded centiseconds, seconds zero-padded microseconds centiseconds
times seconds field % 60 % 60 % 60 % 60 % clock-tick (zsh's own arithmetic)
kill -l %2d) SIG%s, five per row every slot to NSIG-1, one bare name per line (IOT for 6, SIG<n> where unnamed) two columns with the strsignal text every slot from 0 to NSIG-1, one per line (the number where unnamed) one space-separated line, then the real-time signals on Linux
hash hits/command table name=path name=path (reached via its own hash alias → alias -t) the bare path name=path
set -o 27 names, name<TAB>state 38 names under "Current option settings", POSITIVE sense (clobber on) 35 names in four column-major columns 17 names under "Current option settings" (18 on Linux, where Debian's dash adds privileged) zsh's ~180
set +o set -o NAME per line one line: set --default + the non-default ON options one line: set -o .reset -o NAME … set -o NAME per line zsh's form
trap -l the kill -l table rejected, exit 2 rejected, exit 1 rejected, exit 2 silent no-op, exit 0
error in a special builtin continues (exit 2 from the builtin) aborts the script aborts aborts continues

A privileged shell (effective uid ≠ real uid) reads no user file at all; the Korn and Bourne drop-ins take /etc/suid_profile in place of the profile pair.

The zshrs-original line-editor engines are off in every parity mode, --zsh included, and on only when zshrs is being itself. None of the shells zshrs stands in for renders autosuggestion ghost text or colours the command line as you type, so a drop-in that did could not look like its reference. ZSHRS_NATIVE_ZLE_FX overrides the default in both directions: =1 brings the engines back inside a drop-in, =0 turns them off natively. The OSC 133 shell-integration markers are gated the same way.

POSIX requires that "if there is an error in a special built-in utility … the shell shall abort", and ksh93u+m, mksh and dash all do — readonly -X, export -X or shift 99 end the script. bash only does so under set -o posix, and zsh never does, so the drop-ins follow their own shell. The boundary is an ERROR, not a non-zero status: eval false, trap "" BOGUS and unset novar return normally in every one of them, and the harness pins those negatives so the rule cannot widen into "any failing special builtin kills the script".

Three known divergences are deliberate rather than pending. ulimit -a still uses zsh's layout in every mode: the four shells disagree on the resource SET as well as the labels (bash 11 rows, ksh93 24, mksh 10, dash 10), and the set is platform-dependent, so a faithful table has to be built per platform rather than captured from one machine. And --ksh's echo interprets backslash escapes: ksh93's echo deliberately follows the host's /bin/echo, so it interprets on Linux/SysV and does not on macOS — the drop-in follows the XSI convention rather than encoding one platform's. Finally set -X is accepted in every mode: zsh takes -X as a valid option letter and so does zshrs, where ksh93 and dash reject it — matching that needs a per-shell table of valid set letters, which is not built.

Test corpus parity

Suite Tests Coverage
parity 49,256 Differential assertions against the zsh oracle — expansion, completion, builtins, modules, job control, diagnostics
zsh_construct_corpus 396 Every sh/zsh construct outside modules
no_tree_walker_dispatch 174 Behavioral pins for the no-tree-walker invariant
zsh_corpus_via_new_pipeline 123 Native lex+parse+ZshCompiler path
zsh_parser_probe 87 AST-shape probes for every construct
compile_zsh_smoke 29 Per-construct bytecode-level smoke
tree_walker_absent 8 Source-level absence checks (anti-regression)
ztst_runner 77 files / 2,796 chunks Real .ztst files from upstream zsh — see Compatibility measurement for the chunk-level score
Total 50,073 excluding ztst chunks; see below for the ztst figure

Compatibility measurement

Three independent measurements, all re-runnable. The oracle is the zsh development tree the port follows (upstream master, the C source its c: citations point at), not a released zsh. tests/parity/oracle.rs resolves it once per run: $ZSHRS_ORACLE_ZSH, then ~/.cache/zshrs/zsh-oracle/bin/zsh (built from ~/forkedRepos/zsh by scripts/build_zsh_oracle.sh, with its modules and function library), then the system zsh, so a machine without the build still runs the suite against a released zsh. CI builds the same tree at a pinned commit (.github/actions/zsh-oracle, cached per runner) for the Test and Emulation parity jobs and the fuzz modes whose divergences from 5.9.2 are upstream changes (loop, pcre, special, trap, func, subscript, prompt, jobs). The current build reports ZSH_VERSION=5.9.999.3-test, ZSH_PATCHLEVEL=zsh-5.9.0.3-test-476-g8cc5ead.

zshrs itself reports the release it targets (ZSH_VERSION=5.9.2), pins zparseopts and zformat to that release's semantics, and writes .zwc files whose header carries that version, which a shell of another version refuses. The tests that pin one of those compare against a second reference, the zsh 5.9.2 release: tests/parity/oracle.rs release_zsh_path() resolves $ZSHRS_RELEASE_ZSH, then ~/.cache/zshrs/zsh-5.9.2/bin/zsh, then a system zsh, and returns it only when it reports exactly zshrs's $ZSH_VERSION; with none, those tests skip. Build it with ZSHRS_ORACLE_PREFIX=$HOME/.cache/zshrs/zsh-5.9.2 scripts/build_zsh_oracle.sh <tree checked out at zsh-5.9.2>; CI builds it with the same .github/actions/zsh-oracle action (commit: zsh-5.9.2).

Differential parity suite — tests/parity/ is the largest measurement here: hand-written assertions run against the oracle, comparing stdout, exit status and (for diagnostics) stderr.

scripts/build_zsh_oracle.sh      # once, and after the zsh tree moves
cargo test --test parity

Last full run against the master oracle (2026-10-04, recorded in commit 93fed55c6d): 49,116 passed, 140 failed. All 140 were upstream master changes zshrs had not ported yet when the oracle switched from the system 5.9.2 binary; the ports follow as their own commits (typeset -n rules, (w) out-of-range subscripts, ${v/pat/rep} quoting, the restricted-shell removal, message wording). Ignored cases are documented gaps, each #[ignore]d with a citation and an entry in docs/BUGS.md.

Differential fuzz — bins/parity-fuzz.rs generates seed-replayable snippets per grammar mode, runs them through both shells, and flags any stdout/exit divergence. Every case is re-confirmed 3x (--verify 3) so a single load-induced flake cannot register as a divergence.

parity-fuzz --mode <mode> --count 300 --verify 3 --timeout-ms 20000
parity-fuzz --shell csh --count 300 --verify 3     # zshrs --csh vs tcsh, csh grammar
zsh oracle emulation targets
Cases 22,200 (74 modes) 2,100 (7 shells)
Divergences 18 (0.08%) 709 (33.8%)
Modes/targets at zero 73 of 74 --

All residual zsh-mode divergence is in one mode, with a single identified root cause (quote and zmv, the other two modes that diverged, were re-measured at 0 on 2026-10-01):

Mode Count Root cause
unicode 18 unsetopt multibyte is not honoured. zsh drops to BYTE semantics (so [[:alpha:]] matches one byte of a multibyte character); zshrs stays in character mode. All 18 cases set the option.

Two further notes on reading this table:

  • prompt shows 0 divergences but a reproducible ~10-12% timeout rate at a 20 s limit; the harness reports timeouts separately and does not attribute a side.
  • Run the sweep DETACHED (nohup, CI, a supervisor) and the shell inherits SIGQUIT as SIG_IGN. zsh records that as an ignored trap and lists trap -- '' QUIT (c:Src/init.c:1444-1445), and an inherited SIGHUP ignore clears the HUP option (c:1451-1452). zshrs reached neither, because its -c and script-file dispatch bypass init_signals; the trap mode reported ~114/300 in that context and 0 in a foreground shell. Fixed by porting just those two records onto the bypass paths (src/extensions/startup_signals.rs) — NOT by calling init_signals there, which also installs C's SIGCHLD handler and regresses pipeline and job reaping badly (measured: pipeline 0 -> 139). Converging those dispatch paths onto zsh_main is still the structurally correct fix. The recording is ZSH-only and gated off in the drop-in modes: with both signals inherited-ignored, trap prints the QUIT line in zsh, both signals SIG-prefixed in bash, and nothing at all in dash/ksh/sh.

Per emulation target, out of 300 each: ksh 163, mksh 130, pdksh 130, bash 112, sh 80, dash 47, ash 47. The emulation modes are held to a weaker contract than zsh mode -- exit-status sign rather than exact code -- because the reference shells disagree with each other on exact codes.

zsh's own test suite, against zsh -- scripts/ztst_compsys.py runs every Test/*.ztst file of a built zsh tree twice, once with zsh as the shell under test and once with zshrs, and counts only the assertions zsh itself passes in that build:

scripts/ztst_compsys.py --zsh-build <built zsh tree> --core            # zshrs
scripts/ztst_compsys.py --zsh-build <built zsh tree> --core --baseline # zsh

Against zsh 5.9.2 built from the zsh-5.9.2 tag, main at 431cc21de5 passes 2,068 of the 2,068 assertions zsh passes (core suite 1,890 of 1,890, completion suite Y01-Y03 178 of 178). The cargo, TAB, surface and --zsh fuzz measurements (49,300 cargo parity tests pass, 468 of 488 TAB screens match, 1,275 of 1,289 names match) are in docs/parity_report.html.

Upstream ztst corpus, in-tree runner -- the .ztst files copied into this repo from zsh's development tree, run by tests/ztst_runner.rs with no zsh oracle:

cargo test --test ztst_runner -- --nocapture   # per-file chunk tallies

2,625 of 2,796 chunks pass; 110 fail, 61 skip across 77 files (ZTST_TIMEOUT_MS=20000, debug build, main 17bf07d5ed, run while other builds loaded the machine). 45 of those 110 are %prep timeouts that mark a whole file failed: Y04regexargs 6, Y05describe 8, Y06values 26, Y07call_program 5. This corpus tracks zsh's development tree and has no oracle, so it also scores assertions a released zsh fails: run through scripts/ztst_compsys.py --baseline on the same tree, zsh 5.9.2 passes 2 of K01nameref's 133 assertions. The oracle-backed figure is the one to quote.

Read the cargo line with care: the 77 file-level tests report ok unconditionally. run_ztst prints NOTE: N failures … (baseline mode — not failing CI) and never asserts, so "77 passed, 0 failed" measures nothing. The chunk tally — summed from the per-file lines under --nocapture, or printed whole by cargo test --test ztst_runner -- --ignored ztst_summary --nocapture — is the real figure. On a loaded machine a file's %prep can exceed the timeout and report every chunk failed; rerun that file alone before believing it.

tests/gen/ztst_failures.rs pins each failing chunk as an #[ignore]d test, regenerated by cargo test --test ztst_runner -- --ignored gen_ztst_failures. Files with failing chunks in the 17bf07d5ed run, excluding the four timeouts: A02alias 2, B02typeset 3, B06fc 3, D04parameter 3, E01options 1, E02xtrace 1, E03posix 2, K01nameref 8, K02parameter 1, V01zmodload 2, V10private 1, V12zparseopts 12, V13zformat 3, W01history 7, W02jobs 1, X06termquery 7, Z04zgetopt 8. E03posix 13 and V01zmodload 35 are f-flagged chunks documenting zsh bugs, where zshrs gives the correct answer and ztst counts it as a failure. X06termquery fails because zle does not send terminal queries yet.

The gap between the fuzz figure and the ztst figure is intentional and worth reading carefully: the fuzzer samples the grammar it knows how to generate, while ztst samples what zsh's own authors thought worth testing. The ztst number is the one that predicts daily-driver trust.


[0x0B] ARCHITECTURE

The codebase is structurally divided into ported code vs extensions, with the boundary mechanically enforced by tests/port_purity.rs. Bots, contributors, and humans all read docs/PORT.md before writing a single line.

                  ┌────────────────────────────────────────────────────────────────┐
                  │                        zshrs workspace                         │
                  │            4 crates · 991 .rs files · 1,053k lines             │
                  ├──────────────────────────────────────────┬─────────────────────┤
                  │      src/ (512 .rs — runtime crate)      │  vendor/fish/ (157) │
                  │  ┌────────────────────────────────────┐  │  reader / line edit │
                  │  │  src/ported/  (106 — STRICT PORT)  │  │  syntax highlight   │
                  │  │  every .rs ↔ a real Src/<x>.c file │  │  autosuggest        │
                  │  │  every fn carries `/// Port of …`  │  │  abbreviations      │
                  │  │  enforced by tests/port_purity.rs  │  │  env dispatch       │
                  │  │  builtins/ · zle/ · modules/ ·     │  │  history backend    │
                  │  │  hist · jobs · params · pattern ·  │  │  process control    │
                  │  │  signals · glob · subst · math ·   │  │  event system       │
                  │  │  prompt · utils · init · …         │  ├─────────────────────┤
                  │  └────────────────────────────────────┘  │  parse + lex now    │
                  │  ┌────────────────────────────────────┐  │  live IN-RUNTIME    │
                  │  │  src/extensions/ (130 — NON-PORT)  │  │  (folded from the   │
                  │  │  features zsh C does NOT have:     │  │  old parse crate)   │
                  │  │  AOT · plugin/script/autoload      │  ├─────────────────────┤
                  │  │  cache · fish_features · worker    │  │  daemon/ (43 .rs)   │
                  │  │  pool · zwc · arith_compiler ·     │  │  zshrs-daemon — IPC │
                  │  │  hooks · keymaps · widgets ·       │  │  · HTTP · OpenAPI · │
                  │  │  daemon_presence · log · …         │  │  fsnotify · cache · │
                  │  └────────────────────────────────────┘  │  zsource/zhistory/  │
                  │  ┌────────────────────────────────────┐  │  zjob builtins      │
                  │  │  src/recorder/  (1 — feature gate) │  ├─────────────────────┤
                  │  │  AOP intercept; #[cfg(recorder)]   │  │ compsys folded into │
                  │  │  → zero bytes in default binary    │  │ runtime (was its    │
                  │  └────────────────────────────────────┘  │ own crate)          │
                  ├──────────────────────────────────────────┴─────────────────────┤
                  │    bins/  (5 .rs — 3 shipped + bench-autoload, parity-fuzz)    │
                  │     zshrs            zshrs-recorder         zd                 │
                  │     (default)        (--features recorder)  (--features zd)    │
                  ├────────────────────────────────────────────────────────────────┤
                  │                      fusevm (bytecode VM)                      │
                  │            235 opcodes · fused superinstructions · JIT         │
                  └────────────────────────────────────────────────────────────────┘

Directory rule (PORT.md)

Directory Rule Enforcement
src/ported/ Strict 1:1 port. Every .rs mirrors a real upstream zsh Src/<x>.c; every top-level fn carries /// Port of <cname>() from Src/<file>.c:NNNN; no invented helpers; directory and file set FROZEN (106 files, no new files allowed). tests/port_purity.rs
src/compsys/ported/ 1:1 mirror of zsh's Completion/ tree. Engine functions (Base/{Completer,Core,Utility,Widget}, Zsh/Context, plus engine-only entries in Unix/Type, Zsh/Type, Zsh/Command, Unix/Command, and top-level compinit/compdump) are ported to Rust as <name>.rs and carry a Port of _<NAME> header citing the upstream shell source. End-user shell completers (*/Command, Zsh/Function, end-user type files) are copied as-is alongside the Rust ports — same dir layout, same filename, no .rs extension — and dispatched via the _call_function bridge. Current coverage: 991 upstream files mirrored, 250 engine .rs ports. Regenerate the coverage report with scripts/gen_compsys_port_report.py. per-fn tests; doc-comment shell-source citations; gen_compsys_port_report.py
src/extensions/ Non-port only. Features zsh C demonstrably does not have. Must not duplicate or shadow any port. port_purity exempts the 1:1 file rule for this directory only
src/recorder/ Feature-gated. Every symbol #[cfg(feature = "recorder")]; deleted by rustc when off. Cargo.toml required-features = ["recorder"] on the zshrs-recorder bin

[0x0C] EDITOR INTEGRATION

zshrs ships an LSP server and DAP debug adapter built into the binary, plus a JetBrains IDE plugin that drives both.

First Bourne-lineage shell with a language server in its own binary. The idea is not new outside that lineage — Nushell has nu --lsp and Elvish has elvish -lsp (since 0.18.0), and Endo (v0.1.0, F#-inspired, not POSIX) ships LSP plus DAP. Inside it, no shell does: bash, zsh, ksh, dash and fish ship neither --lsp nor --dap. bash-language-server and fish-lsp are separate Node/TypeScript packages that reimplement a parser the shell already has, zsh has no mainstream server at all, and vscode-bash-debug is a VS Code extension shelling out to the external bashdb script. Because the server is the shell, zshrs's LSP answers from the same lexer, parser, option table and compsys completers the interactive shell uses — including live _git / _docker / _ssh completion inside the editor (see below), which no shell language server of any lineage does.

CLI flags

zshrs --lsp                  # LSP server over stdio
zshrs --dap HOST:PORT        # DAP debugger; TCP connect-back to IDE listener
zshrs --dap                  # DAP debugger over stdio (executable-spawned clients)
zshrs --dump-reflection      # JSON dump of builtins / keywords / options
zshrs --dump-plugins         # JSON dump of the plugins the last recorded
                             # session sourced, grouped by manager (zinit /
                             # oh-my-zsh / prezto / antidote / antigen /
                             # zplug); the daemon fills plugins.db from each
                             # recorder shard, so it is empty until one is
                             # ingested; feeds the IDE's External Libraries view
zshrs --docs <name>          # render the LSP hover card for <name>
zshrs --fmt [-w] [-t] [-i N] [FILE…]
                             # format zsh source: block-structure
                             # reindent + idiomatic spacing (dedupe
                             # runs; a;b → a; b, a&&b → a && b,
                             # a|b → a | b, cmd& → cmd &, x;; → x ;;,
                             # f () → f(), bare then/do join onto
                             # the opener as '; then'/'; do' per the
                             # zsh + OMZ style guides), heredoc-safe,
                             # idempotent. Indent defaults to 4
                             # spaces per level; editor tabSize and
                             # CLI -i N override explicitly.
                             # stdin→stdout with no files; -w rewrites
                             # in place; -i sets indent width (default
                             # 4); -t indents with tabs

All six flags dispatch from bins/zshrs.rs into src/extensions/lsp.rs, src/extensions/dap.rs, and src/extensions/plugin_cache.rs. The LSP and DAP modules are dependency-free additions (no lsp-server / lsp-types / dap-types crates) — Content-Length framing + JSON-RPC are hand-rolled on top of serde_json to keep the default build lean.

LSP capabilities (zshrs --lsp)

Capability Trigger
completion builtins, keywords, options, special vars, in-file functions plus live compsys matches — the same _git / _docker / _ssh completers a Tab press drives, so git ch completes checkout and git checkout lists branches (see docs/IN_EDITOR_COMPSYS_COMPLETION.md). Writing a completer is covered too: inside an _arguments spec, the action field offers completer names and the inline action forms ((list), ((val\:desc)), ->state)
hover markdown cards for builtins / keywords / options / special vars
definition / references function names declared in the open document
documentHighlight same scan as references
documentSymbol function foo, foo(), alias, local/typeset/export
foldingRange { … } / do … done / case … esac blocks + ≥3 # comment runs
rename (with prepareRename) word-boundary aware replace across document
semanticTokens/full comment / string / number / keyword / variable / function classes
formatting full syntax-aware reindent + idiomatic spacing (src/extensions/fmt.rs, same engine as --fmt): if/fi, do/done, case arms, { }, ( ), [[ ]], continuations; whitespace dedupe and canonical ; && || | & ;; spacing with quote/${…}/fd-redirect/glob-pattern guards; heredoc bodies verbatim
publishDiagnostics brace + block matching, unclosed strings, lights up on didOpen / didChange / didSave

Trigger characters for completion: $, {, -, :. Optional ZSHRS_LSP_LOG=<path> env var dumps every request/response for debugging.

Compsys completion reads the recorded environment from the canonical rkyv shard (~/.zshrs/images/*-recorder.rkyv) — compdef map, fpath, autoload stubs, zstyle — so it needs zshrs-recorder to have run at least once; without a shard the LSP still completes everything else in the table above. Completers that shell out (git for-each-ref behind git checkout <tab>) run with their subprocess killed at the request deadline, and never inherit the editor's stdin/stdout.

DAP capabilities (zshrs --dap [HOST:PORT])

Two transports, same DAP server: --dap HOST:PORT connects back to the IDE's TCP listener (JetBrains; keeps stdout free for the script), while bare --dap serves DAP over stdio for clients that spawn the adapter as an executable (e.g. VS Code's DebugAdapterExecutable).

Request Behaviour (v1)
initialize / configurationDone full capability advertisement, emits initialized event
setBreakpoints stored per-file, ack with verified: true
launch spawn zshrs <program> <args> as a child process
threads / stackTrace / scopes single-thread model, one synthetic frame
variables environment snapshot (scope ref 1)
evaluate runs zshrs -c <expr> against current cwd, returns stdout
continue / next / stepIn / stepOut acked (no per-statement pause in v1)
pause emits stopped { reason: "pause" }
disconnect / terminate kills the child process
program stdout / stderr streamed as DAP output events
child exit fires terminated event

Deeper integration (per-statement pause, breakpoint honouring against the live interpreter, scope walk-back into the Param table) is scaffolded via dap::install_hooks(DapHooks { … }) and lands incrementally.

JetBrains plugin (editors/intellij/)

cd editors/intellij
JAVA_HOME=$(/usr/libexec/java_home -v 17) ./gradlew buildPlugin
# → build/distributions/zshrs-intellij-<version>.zip

Install via Settings → Plugins → ⚙ → Install Plugin from Disk….

Plugin features:

  • File types: .zsh + every dot-rc (.zshrc, .zshenv, .zlogin, .zlogout, .zprofile, .zpreztorc).
  • Hand-rolled lexer with 45 independently-themeable color slots (Settings → Editor → Color Scheme → zshrs).
  • LSP client auto-starts zshrs --lsp on first file open. Hover, completion, goto-definition, references, rename, document symbols, semantic tokens, folding, formatting, diagnostics — all wired.
  • Run configurations with toggles for -f/-x/-v/--disasm/ --dump-ast and a compat-mode picker (zsh / bash / ksh / posix / default zshrs).
  • Debugger over DAP TCP socket: line breakpoints from the gutter, step over/into/out/pause, frames panel with source navigation, variables panel (scalars + arrays + assoc arrays), Evaluate dialog, Console streaming program stdout in real time.
  • Reflection tool window (right edge) — left-click any name to open the ANSI-rendered zshrs --docs <name> card.
  • Settings → Tools → zshrs — point at a non-PATH zshrs binary, set LSP extra args + env, configure file extensions, control auto-restart.

Requires a paid JetBrains IDE on 2024.2+ (RustRover, IDEA Ultimate, GoLand, PyCharm Pro, WebStorm, RubyMine, PhpStorm, CLion, Rider, DataGrip, Aqua) because the platform LSP API is not in Community editions.

Other LSP / DAP clients

The stdio LSP and TCP DAP servers are protocol-conformant — any LSP/DAP client works:

# Helix — ~/.config/helix/languages.toml
[language-server.zshrs]
command = "zshrs"
args = ["--lsp"]

[[language]]
name = "bash"
language-servers = ["zshrs"]
file-types = ["zsh", "zshrc", "zshenv", "zlogin", "zlogout", "zprofile"]
-- Neovim (nvim-lspconfig style)
require("lspconfig").configs.zshrs = {
  default_config = {
    cmd = { "zshrs", "--lsp" },
    filetypes = { "zsh", "sh" },
    root_dir = function() return vim.fn.getcwd() end,
  },
}
require("lspconfig").zshrs.setup({})
// VS Code — keybindings.json + a small extension that spawns `zshrs --lsp`
// over stdio works the same as any LSP-backed extension.

See editors/intellij/README.md for the JetBrains plugin's full architecture, debugger internals, and limitation list.


[0x0D] NATIVE RUST PLUGINS

Every shell before zshrs runs plugins as interpreted scripts. zshrs is the first JIT-compiled Unix shell, and the first that hosts plugins written in a native compiled language (Rust), loaded at runtime with no recompile of the shell. A plugin is an ordinary cdylib the shell dlopens through a stable, versioned C ABI (the znative crate). Nothing about Rust's unstable layout, allocator, or panic ABI crosses the boundary — only #[repr(C)] data.

// src/lib.rs — crate-type = ["cdylib"], deps: znative
use znative::{declare_plugin, Args, Host};
use std::os::raw::c_int;

fn rhello(host: &Host, args: &Args) -> c_int {
    let pwd = host.getvar("PWD").unwrap_or_default();
    host.print(&format!("hello, {} (pwd={pwd})\n", args.rest().join(" ")));
    0
}

declare_plugin! {
    name: "hello",
    version: "0.1.0",
    builtins: { "rhello" => rhello },
}
cargo build                              # → target/debug/libhello.dylib (.so on Linux)

# Inside zshrs:
zmodload -R ./target/debug/libhello.dylib   # load
zmodload -R                                  # list loaded plugins
rhello world                                 # native command, no fork, no interpreter
zmodload -uR hello                           # unload by name

Plugin commands resolve after real builtins and shell functions, before PATH lookup — the same slot zsh uses for zmodload -ab autoloaded builtins. The host API a plugin can call back through: print, eval (run shell code), getvar / setvar (shell scalars), getfunction / addfunction (read a function's deparsed body / define one — also deparse-as-a-service), and register_builtin.

Installing plugins with znative

zmodload -R is the primitive; znative is the package manager built on it. One line per plugin in your .zshrc — it installs on the first shell start (clone → cargo build --release for native plugins → zmodload -R) and then loads from a content-addressed store, zero-network, every start after:

znative load MenkeTechnologies/zshrs-forgit    # native (Rust) plugin, self-installing
znative load zsh-users/zsh-autosuggestions     # zsh script plugins work too

Sources also take github:owner/repo, git+URL@ref, and path:DIR. Manage the store with znative list / info / update / remove. Full reference: docs/ZNATIVE.md.

Plugins can also provide native (Rust) completions — a completions: block in declare_plugin! wires a Rust generator into zsh's completion system (compsys), so mycmd <TAB> runs your Rust code to produce candidates. See examples/plugin-complete/.

Porting existing zsh plugins: docs/PORTING_ZSH_PLUGIN.md is a construct-by-construct zsh→Rust guide, worked end-to-end on forgit (the git+fzf plugin, ported command-for-command: ga glo gd gcf gclean gss gcp grh gi) and — for the harder self-reentrant case — git-fuzzy (fzf calling back into the tool per keystroke, with a --listen live-reload watcher).

vs bash / zsh native plugins

Native runtime plugins aren't new — bash enable -f file.so name and zsh zmodload both load native builtins. The difference is the interface:

script plugin (.zsh) bash enable -f / zsh module zshrs Rust plugin
Artifact .zsh (interpreted) .so built in the shell's tree .dylib/.so cdylib
Build against nothing (sourced) the shell's private internal headers published znative crate
Stable ABI n/a none — welded to one shell build, no version gate versioned, load-time checked
Distribution a file to source rebuild per shell release one crates.io SDK crate
Speed interpreted native native
Third-party viable yes (scripts only) rare (needs shell source) yes (cargo add)

bash and zsh load native code only through their internal C APIs — you compile against the shell's private headers, with no stable ABI and no version gate, so a plugin is welded to one build and can crash a mismatched one. That's why neither has meaningful third-party native plugins. zshrs exposes a stable, published, versioned ABI (cargo add znative), version-gated at load — first shell to make its native-plugin interface an independently-published ABI package instead of its own build-tree internals.

A runnable example lives in examples/plugin-hello/. Full guide: docs/PLUGINS.md; package manager: docs/ZNATIVE.md.


[0xFF] LICENSE

MIT — Copyright (c) 2026 MenkeTechnologies

zshrs's own code is MIT. src/ported/ is a port of the zsh C source and vendor/zsh/ is bundled from zsh, so those parts also carry zsh's licence (copyright the Zsh Development Group), and four bundled completion functions are GPL. The licence text and the file list are in NOTICE.md, which ships with every distribution.

Original-authorship record + portability stance: CREATORS.md. Maintainer governance + protected invariants: MAINTAINERS.md.

This is a legacy, not a battle. The canonical register of what originated here is docs/INVENTIONS.md — twenty-nine entries, each filtered by three tests: it exists in the tree with a name you can type, no other shell does the thing at all (with the near misses named), and it is an idea another project could inherit rather than a file layout or a flag. The same document lists what was cut and why, including claims that did not survive a prior-art check. The synthesis it covers (compiled-shell architecture with both bytecode and native code persisted, recorder-owns-rebuild AOP intercept, purity analysis splitting cacheable config from replayed config, the singleton daemon owning every mutation, session-persistent supervised jobs with bidirectional ptmx attach, cross-shell pub/sub + named-lock builtins, bytecode-level value lineage, AOP advice as a shell primitive, a stable published plugin ABI) is prior art for the shell-design commons under the MIT grant. Future shells — bash, fish, nushell, elvish, oil, xonsh, murex, projects that don't exist yet — should inherit any of it. The protected invariants in MAINTAINERS.md guard upstream identity, not the ideas.

Ports must credit zshrs as the invention source in their docs — a one-line attribution in your README / design doc / release notes. Suggested wording:

Inspired by / ported from zshrs by MenkeTechnologies.

Ideas can't be copyrighted so this is an ask, not an MIT-enforced clause; honoring it keeps the legacy traceable. See CREATORS.md § Legacy + § Attribution expectation for the full list + suggested forms.

About

zshrs => zsh Superset

Resources

Stars

17 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages