Every builtin, keyword, parameter-expansion form, ZshFlag, array shape, AOP primitive, parallel primitive, and anti-fork coreutils replacement that zshrs ships. Each entry shows what it does and a runnable example. Coverage gate: 344 construct-corpus tests in tests/zsh_construct_corpus.rs exercise every category below; 817 tests total across all suites pass on the new (default) pipeline. Jump via the chapter index, or Ctrl+F for a specific name.
Control Flow
14 topics · compiled to fusevm bytecode with deferred jump patches; no tree-walker dispatch. Includes anonymous-fn auto-call and case fall-through (;&) / test-next (;|).
# if … then … fi
Standard POSIX conditional. elif branches and an optional else are supported. Status semantics: a Status(0) is truthy in zshrs, so if cmd; then …; fi runs the then arm when cmd exits 0. The compile path emits JumpIfFalse at the end of each conditional probe.
if [[ -d "$dir" ]]; then
cd "$dir"
elif [[ -f "$dir" ]]; then
echo "regular file"
else
echo "no such path"
fi
# while … do … done
Loops while the condition exits 0. Loop-body status is preserved across iterations via a dedicated slot — the loop's exit status is the body's last-command status (or 0 if the body never ran), matching POSIX.
i=0
while (( i < 5 )); do
echo "iter $i"
(( i++ ))
done
# until … do … done
Inverse of while — loops while the condition is non-zero. Compiles to the same bytecode shape with the condition jump inverted.
until ping -c1 -W1 example.com >/dev/null; do
sleep 1
done
echo "online"
# for var in words; do … done
Iterates var over the word list. Words are compiled at runtime — ${arr[@]} splices into N iterations via BUILTIN_ARRAY_FLATTEN. The for-loop preserves break/continue patches as deferred jump sites so nested loops work correctly.
for f in *.rs; do
echo "compiling $f"
done
arr=(alpha beta gamma)
for x in ${arr[@]}; do
echo "got $x"
done
# for ((init; cond; step)); do … done
C-style arithmetic for loop. The arithmetic expressions compile through the inline arithmetic compiler (no $((…)) wrapping needed inside the parens).
for ((i = 0; i < 10; i++)); do
echo "i=$i"
done
# case … esac
Pattern-matched dispatch. Patterns are compiled via compile_case_pattern which preserves $-expansion but does not glob-expand the pattern itself (otherwise case x in *) … would expand * to the cwd listing). Match check uses Op::StrMatch (host-routed glob match).
case "$1" in
start) echo starting ;;
stop) echo stopping ;;
*) echo "usage: $0 {start|stop}"; exit 1 ;;
esac
# select var in words; do … done
Interactive numbered-menu loop. Prints 1) word1\n2) word2\n… to stderr, prompts via $PROMPT3 (default ?# ), reads stdin, sets var to the chosen word, runs the body. $REPLY contains the raw input. EOF on stdin exits the loop. The real break keyword exits early via the cross-VM LoopSignal mechanism; the legacy BREAK_SELECT=1 sentinel still works for back-compat.
select choice in build test deploy quit; do
case $choice in
build) cargo build ;;
test) cargo test ;;
deploy) ./deploy.sh ;;
quit) break ;;
esac
done
# () { body } args…
Anonymous function. Defined and immediately invoked with the trailing words as $1…$N. Parser routes Inoutpar (the () token) to parse_anon_funcdef, which generates a unique _zshrs_anon_N name; compile_funcdef registers and calls when auto_call_args is set. () with no body falls back to an empty subshell (POSIX no-op).
() { echo "hi $1, $2 args"; } world 1
# → hi world, 1 args
# case;& fall-through, ;| test-next
Per-arm separators after a case body. ;; exits (default), ;& falls through to the next arm without re-testing its pattern, ;| falls through and does re-test. compile_case tracks a pending_fall patch site so ;& emits a forward jump that the next arm's body-start patches.
case "$x" in
a) print A ;& # falls into b's body
b) print B ;;
*) print other ;;
esac
# coproc [name] { body }
Forks the body with bidirectional pipes. Two pipes are created (parent→child stdin, child→parent stdout); child setsids and runs the body, parent stores [read_fd, write_fd] in $COPROC (or the given name as an array). Read child output via /dev/fd/${COPROC[1]}, write to its stdin via /dev/fd/${COPROC[2]}. Job-table integration is Phase G6.
coproc { while read line; do echo "ECHO: $line"; done }
echo hello > /dev/fd/${COPROC[2]}
read reply < /dev/fd/${COPROC[1]}
echo "$reply" # ECHO: hello
# break
Exits the innermost for/while/until. Compiled as a deferred forward Jump(0) patched to land just past the loop's exit point. Inside select use BREAK_SELECT=1 instead until cross-construct loop control lands in Phase G6.
for f in *.log; do
[[ -s $f ]] || break # stop on first empty
process "$f"
done
# continue
Skip to the next iteration. Same patch mechanism as break — patches into the loop's continue target which is the iteration step (e.g. PreIncSlotVoid in the for-loop).
for n in 1 2 3 4 5; do
(( n % 2 == 0 )) || continue
echo "even: $n"
done
# return [n]
Return from a function with the given status (default last-status). Compile emits a forward Op::Jump(0) patched to past the chunk's end; standalone-chunk VMs interpret this as halt. Op::Return alone restarts the body from ip=0, which is wrong for top-level scripts.
greet() {
[[ -z "$1" ]] && return 1
echo "hello, $1"
}
# ; && || &
Sequential, conditional, and background separators. cmd1 && cmd2 runs cmd2 only if cmd1 succeeded; || is the inverse. & backgrounds the preceding command via BUILTIN_RUN_BG — fork + setsid, parent returns Status(0) immediately. Compiled with deferred short-circuit jumps to avoid double-compilation of the next command.
git pull && cargo test || echo "test failed"
sleep 30 &
echo "going to bg"
Arrays & Associative Arrays
13 topics · indexed arrays in executor.arrays, assoc in executor.assoc_arrays; argv splice via fusevm 0.10.1+.
# arr=(a b c)
Indexed-array literal assignment. Compile emits N element pushes + name push, then BUILTIN_SET_ARRAY (id 287) which writes a Vec<String> into executor.arrays and clears any prior scalar binding for the same name.
arr=(alpha beta "two words" gamma)
echo ${#arr[@]} # 4
# arr+=(d e)
Append elements to an existing array (or create if missing). Routes through BUILTIN_APPEND_ARRAY (id 295) which extends executor.arrays[name].
arr=(a b)
arr+=(c d)
echo ${arr[@]} # a b c d
# ${arr[N]}
Indexed access. zsh is 1-based for positive indices; negative indices count from the end (${arr[-1]} = last). Routes through BUILTIN_ARRAY_INDEX (id 289). For assoc arrays the same form does string-key lookup.
arr=(alpha beta gamma)
echo ${arr[1]} # alpha
echo ${arr[-1]} # gamma
# ${arr[@]}
Splice all elements as separate argv slots. Pushes a Value::Array which fusevm's Op::Exec/Op::ExecBg/Op::CallFunction and pop_args flatten into individual args. Without the splice, the array would collapse to one space-joined scalar.
arr=(--verbose --color=always /etc /var)
ls "${arr[@]}" # ls --verbose --color=always /etc /var
# ${#arr[@]}
Number of elements. Routes through BUILTIN_ARRAY_LENGTH (id 291).
arr=(a b c d)
echo ${#arr[@]} # 4
# for x in ${arr[@]}
Iterate over array elements. The for-loop word-list compile path calls BUILTIN_ARRAY_FLATTEN (id 293) which flattens nested arrays one level — so for x in start ${arr[@]} end produces N+2 iterations, not 3.
arr=(red green blue)
for c in ${arr[@]}; do
echo "color: $c"
done
# arr=()
Empty-array literal. ${#arr[@]} returns 0; iteration runs zero times.
arr=()
for x in ${arr[@]}; do echo "never"; done
echo "len=${#arr[@]}" # len=0
# typeset -A name / declare -A name
Declare an associative array. The executor's builtin_typeset handles -A and pre-creates the HashMap<String, String> entry in executor.assoc_arrays. Optional — name[key]=val creates the assoc on demand too.
typeset -A user
user[name]=Jacob
user[role]=eng
echo "${user[name]}" # Jacob
# name[key]=value
Assoc-array key set. Compile detects name[key] shape on the LHS of an assignment and routes to BUILTIN_SET_ASSOC (id 288) which stores into executor.assoc_arrays[name][key].
db[host]=localhost
db[port]=5432
db[user]=admin
echo "${db[host]}:${db[port]}"
# ${name[key]}
Assoc-array lookup. BUILTIN_ARRAY_INDEX checks assoc_arrays first; if the name has an assoc binding the string key is used directly. Missing keys return empty string.
typeset -A m
m[a]=1; m[b]=2
echo "a=${m[a]} b=${m[b]}"
echo "missing=[${m[nope]}]" # missing=[]
# ${(k)name}
List the keys of an associative array. Order is HashMap iteration order (implementation-defined). See the Zsh Parameter Flags chapter for the full flag set.
typeset -A m
m[apple]=1; m[banana]=2
for k in "${(k)m}"; do echo $k; done | sort
# ${(v)name}
List the values of an associative array. ${name[@]} on an assoc returns the same value list.
typeset -A m
m[apple]=1; m[banana]=2
for v in "${(v)m}"; do echo $v; done | sort
# m[k]=new overwrites
Re-assigning a key replaces the prior value. The set-builtin always inserts; use += for append-on-existing-key semantics.
m[k]=first
m[k]=second
echo "${m[k]}" # second
# m[k]+=tail
Append onto an existing assoc value (string concat). If the key is missing, behaves like a plain set. Routes through BUILTIN_APPEND_ASSOC (id 298).
m[host]=localhost
m[host]+=":5432"
echo "${m[host]}" # localhost:5432
Parameter Expansion
21 topics · 15 of 19 VarModifier variants lower to native fusevm ops via Op::ExpandParam; ${var:-default} + ${#var} have been native-lowered in compile_zsh.rs (Phase 1 steps 1–2); 4 remaining variants + stacked ZshFlags + $(cmd)/$((expr))/concat ${a}${b} still hit the runtime fallback or the untokenize_preserve_quotes bridge to the legacy ShellParser.
# ${var:-default}
Use default if var is unset or empty. Lowers to Op::ExpandParam(DEFAULT). The default expression is itself compiled (variables, command substitution, etc. all expand at use time).
name=${1:-anonymous}
log_level=${LOG_LEVEL:-info}
target=${1:-$(date +%Y%m%d)}
# ${var:=default}
Assign default to var if unset/empty, then expand. Lowers to Op::ExpandParam(ASSIGN). The variable is set in executor.variables as a side effect.
: ${CACHE_DIR:=$HOME/.cache}
echo "$CACHE_DIR" # CACHE_DIR is now set
# ${var:?msg}
Print error and exit if var is unset/empty. Lowers to Op::ExpandParam(ERROR). Common in script preamble for required env vars.
: ${API_TOKEN:?must be set}
: ${DEPLOY_TARGET:?usage: deploy <target>}
# ${var:+alt}
Expand to alt if var is set and non-empty, else empty. Lowers to Op::ExpandParam(ALTERNATE).
flag=${VERBOSE:+--verbose}
cargo build $flag
# ${#var}
String length of scalar (or via ${(#)arr} the element count of an array — see Zsh Flags). Lowers to Op::ExpandParam(LENGTH).
name=Jacob
echo ${#name} # 5
# ${var:offset:length}
Substring extraction. Lowers to Op::ExpandParam(SLICE). Negative offsets count from the end. Length is optional (omitted = to end).
s=abcdefgh
echo ${s:2:3} # cde
echo ${s:-3} # fgh (last 3 chars)
echo ${s:2} # cdefgh (offset 2 to end)
# ${var#pat} / ${var##pat}
Strip shortest / longest matching prefix. # = shortest, ## = longest. Lowers to STRIP_SHORT / STRIP_LONG.
path=/usr/local/bin/zshrs
echo ${path##*/} # zshrs (basename via longest-prefix-strip)
echo ${path#*/} # usr/local/bin/zshrs
# ${var%pat} / ${var%%pat}
Strip shortest / longest matching suffix. % = shortest, %% = longest. Lowers to RSTRIP_SHORT / RSTRIP_LONG.
file=archive.tar.gz
echo ${file%.gz} # archive.tar
echo ${file%%.*} # archive (dirname via longest-suffix-strip)
# ${var/pat/repl}
Replace first match of pat with repl. Lowers to SUBST_FIRST. repl may use & for the matched text in some shells; zshrs follows zsh's literal-replace semantics.
s="foo bar foo"
echo ${s/foo/baz} # baz bar foo
# ${var//pat/repl}
Replace all matches. Lowers to SUBST_ALL. Heavily used by zpwr-style idioms (path normalization, sigil replacement).
s="hello world hello"
echo ${s//hello/HI} # HI world HI
path="/a/b/c"
echo ${path//\//.} # .a.b.c
# ${var:u} / ${(U)var}
Uppercase. The :u postfix lowers to Op::ExpandParam(UPPER); the (U) flag form goes through BUILTIN_PARAM_FLAG and supports stacking with other flags.
x=hello
echo ${x:u} # HELLO
echo ${(U)x} # HELLO
# ${var:l} / ${(L)var}
Lowercase. Same dual-form pattern as upper.
x=HELLO
echo ${x:l} # hello
echo ${(L)x} # hello
# $?
Exit status of the last command. Routed through BUILTIN_GET_VAR("?") which reads vm.last_status synced into the executor.
cargo test
if (( $? != 0 )); then echo "tests failed"; fi
# $$
Current shell PID (std::process::id()).
tmpdir=/tmp/build_$$
mkdir -p "$tmpdir"
# $1 $2 … $@ $* $#
Positional parameters. $@ / $* expand to the full list (joined by IFS), $# is the count, numeric $N is the Nth positional. Native fast-path: "$@" and ${arr[@]} reach BUILTIN_GET_VAR / BUILTIN_ARRAY_ALL as Value::Array and spread through BUILTIN_ARRAY_FLATTEN.
greet() {
echo "got $# args"
for arg in "$@"; do echo "- $arg"; done
}
greet alpha "two words" gamma
# $! $_ $-
Auxiliary special params. $! = PID of the most recent backgrounded job. $_ = last argument of the previous command (or empty at script start). $- = current shell flag set as a single string. All routed through BUILTIN_GET_VAR.
# $RANDOM $SECONDS $EPOCHSECONDS $LINENO
Dynamic special parameters resolved on each read in get_variable:
$RANDOM — 15-bit pseudorandom int. Mixes nanos+pid via Knuth's hash, masked to 15 bits. No seedable state today (independent draws).
$SECONDS — seconds since shell start, derived from __zshrs_start_secs baseline.
$EPOCHSECONDS — Unix time, SystemTime::now().
$LINENO — current line in the script (falls back to 1 today; full line tracking is Phase G work).
tmp="/tmp/build_$$_$RANDOM"
echo "elapsed: $SECONDS s"
echo "now: $EPOCHSECONDS"
# $PIPESTATUS / $pipestatus
Per-stage exit codes of the most recent pipeline. BUILTIN_RUN_PIPELINE collects each stage's status and writes both pipestatus (zsh convention) and PIPESTATUS (bash convention). Useful for set -e-style scripts that need to differentiate which stage failed.
seq 100 | grep '^5' | wc -l
echo "${pipestatus[@]}" # 0 0 0
false | true
echo "${pipestatus[1]}" # 1 (zsh: 1-indexed)
# [[ -v var ]] — existence test
BUILTIN_VAR_EXISTS (id 306) checks every binding table — scalar variables, arrays, assoc_arrays, plus the process env — and returns true if any matches. Useful for "set but empty" vs "unset" disambiguation that -z conflates.
x=""
[[ -v x ]] && echo "set" # set (even though empty)
[[ -z $x ]] && echo "empty" # empty
unset x
[[ -v x ]] || echo "unset" # unset
# $((expr))
Arithmetic substitution. The expression compiles through the inline arithmetic compiler — no runtime parser invocation. Integers are i64; supports + - * / % ** & | ^ << >> && || and pre/post inc/dec.
(( total = 1 + 2 * 3 ))
echo $((2 ** 16)) # 65536
echo $((0xff & 0x0f))
# $(cmd) / ` cmd `
Command substitution. zshrs runs the inner command on a nested VM and captures stdout via os_pipe::pipe() + dup2 — no fork. Trailing newlines are stripped per POSIX.
now=$(date +%s)
files=$(ls *.rs | wc -l)
echo "$files files at $now"
Zsh Parameter Flags
12 topics · ${(flags)name} form. Flags apply left-to-right; (jL) joins-then-lowercases, (s:,:U) splits-then-uppercases. Routes through BUILTIN_PARAM_FLAG (id 297).
# (L) — lowercase
Lowercases the value. For arrays, lowercases each element.
x=Hello
echo ${(L)x} # hello
arr=(One Two Three)
echo ${(L)arr} # one two three
# (U) — uppercase
Uppercases.
echo ${(U)x} # HELLO
# (j:sep:) — join
Join array elements with sep. The delimiter char following j sets the bracket — :, ., ,, | are common. j with no delimiter joins with a single space (IFS-default).
arr=(one two three)
echo ${(j:-:)arr} # one-two-three
echo ${(j:|:)arr} # one|two|three
echo ${(j)arr} # one two three
# (s:sep:) — split
Split a scalar on sep into an array. The result splices into argv via the standard array-flatten path.
csv="alpha,beta,gamma"
for x in "${(s:,:)csv}"; do
echo "[$x]"
done
# (f) — split on newlines
Shorthand for (s:\n:) — split on newlines. Common with command substitution.
for line in "${(f)$(ls)}"; do
echo "FILE: $line"
done
# (o) — sort ascending
Lexicographic sort.
arr=(charlie alpha bravo)
echo ${(o)arr} # alpha bravo charlie
# (O) — sort descending
Reverse lexicographic sort.
echo ${(O)arr} # charlie bravo alpha
# (P) — indirect
Use the value of the variable as another variable name and look that up. Useful for dynamic dispatch tables.
real=42
ref=real
echo ${(P)ref} # 42
# (@) — force array
Coerce a scalar to a single-element array (so subsequent flags treat it as array-shape).
x=hello
echo ${#x} # 5 (string length)
echo ${(#)${(@)x}} # 1 (array length after @-coerce)
# (k) — keys of assoc
Returns the keys of an associative array as an array. Order is HashMap iteration order.
typeset -A m
m[apple]=1; m[banana]=2
for k in "${(k)m}"; do echo "$k"; done
# (v) — values of assoc
Returns the values of an associative array as an array.
echo ${(v)m} # 1 2
# (#) — count
Element count for arrays, character count for scalars. Different from ${#var} in that it can be combined with other flags via stacking.
arr=(a b c d)
echo ${(#)arr} # 4
echo ${(#L)x} # length after lowercasing
# (q) / (qq) / (qqq) — quote
Wrap each value with shell-safe quoting. Consecutive qs raise the quoting level: q = POSIX single-quote (escaping inner '), qq = double-quote (escaping $/`/"/\), qqq = ANSI-C $'…' form (escaping control chars + backslashes).
x="hi 'world'"
echo ${(q)x} # 'hi '\''world'\'''
echo ${(qq)x} # "hi 'world'"
s=$(printf 'a\tb')
echo ${(qqq)s} # $'a\tb'
# (g) — backslash-escape unwrap
Process backslash escapes (\n, \t, \r, \\, \xNN). Useful when reading lines from a config that stored real newlines as literal \n.
s='hello\nworld'
echo "${(g)s}"
# hello
# world
# (n) — natural-numeric sort
Compare digit runs as integers and other runs lexicographically. file2 sorts before file10 (vs lex order which would put file10 first). Combine with (o)/(O) for ascending/descending.
arr=(file10 file2 file1 file20)
echo ${(on)arr} # file1 file2 file10 file20
# (i) — case-insensitive sort
Sort comparing lowercase forms while preserving original case in the output.
arr=(Banana apple Cherry)
echo ${(i)arr} # apple Banana Cherry
# (t) — type query
Returns the variable's typeset shape: scalar, array, association, or empty (unset). Useful in scripts that branch on whether a name is an indexed vs assoc array.
arr=(a b)
typeset -A m; m[k]=v
sc=str
echo "${(t)arr}|${(t)m}|${(t)sc}"
# array|association|scalar
# (%) — prompt expansion
Process %F/%B/%f/%{/%} etc. via expand_prompt_string. Useful for storing prompt fragments in variables and rendering them at use time.
fragment='%F{cyan}>>%f '
echo "${(%)fragment}"
# (e) — re-evaluate
Run the value as a shell command and return its captured stdout. Equivalent to $(eval "$value") but uses the in-process pipe-capture path (no fork). Late-bound config strings.
cmd='echo "user is $(whoami)"'
echo "${(e)cmd}" # user is jacob
# (p) — print-style escapes
Process backslash escapes the same way print -e does. Same set as (g) with \e / \E mapped to ESC.
fmt='\e[36mcolor\e[0m'
echo "${(p)fmt}" # cyan "color"
Redirection & Pipelines
14 topics · pipelines fork-per-stage via BUILTIN_RUN_PIPELINE (id 285); redirects scoped via WithRedirectsBegin/End.
# cmd > file — write
Truncates and writes stdout to file. Bracketed in a redirect scope so subsequent commands see the original fd 1.
echo hello > out.txt
# cmd >> file — append
Appends stdout. Creates the file if missing.
date >> access.log
# cmd < file — read
Connects file to stdin.
sort < data.txt
# cmd 2> file — stderr
Redirect stderr (fd 2). Use 2>&1 to merge into stdout.
cargo build 2> build.err
make 2>&1 | tee build.log
# cmd &> file — write both
Redirect both stdout and stderr (zsh/bash extension). &>> appends.
./run.sh &> combined.log
# cmd <<EOF — heredoc (with variants)
Multi-line stdin literal. Three flavors:
<<EOF — variables, command sub, arithmetic all expand inside the body. Body trailing newline trimmed before HereString emit so stdin is byte-identical to source.
<<-EOF — leading tabs stripped from each body line and from the closing-terminator-line check. Lets you indent the heredoc body inside a function body.
<<'EOF' / <<"EOF" — quoted terminator → body is verbatim, no expansion. Detected by SNULL/DNULL markers in the lexer's terminator string; HereDocInfo.quoted drives compile-side behavior.
Body capture happens in a parser post-pass (fill_heredoc_bodies) — the lexer collects bodies into self.heredocs[] at process_heredocs (called on Newlin/Endinput); the parser records a heredoc_idx per redir during parse_redirection; the post-pass walks the AST resolving indices into ZshRedir.heredoc.
cat <<EOF
hello $USER
host: $(hostname)
EOF
cat <<-EOF
indented
body
EOF
cat <<'EOF'
$USER stays literal
EOF
# cmd <<< "string" — herestring
Pass a single string (with trailing newline) as stdin.
tr a-z A-Z <<< "hello" # HELLO
# cmd1 | cmd2 — pipeline
Connect cmd1's stdout to cmd2's stdin via a pipe. zshrs's pipeline is bytecode-native: each stage compiles to a sub-chunk; BUILTIN_RUN_PIPELINE creates N-1 pipes, forks N children, wires fds, runs each stage's bytecode on a fresh VM. SIGPIPE works correctly.
seq 100 | sort | uniq | wc -l
# cmd1 |& cmd2 — pipe stdout+stderr
zsh extension: pipes both fd 1 and fd 2 of the LHS to RHS.
cargo test |& head -40
# ! cmd — invert status
Negate the pipeline's exit status. ! cmd exits 0 if cmd failed, 1 if cmd succeeded.
! grep -q ERROR log # status 0 means no ERROR found
# <(cmd) — process sub (input)
Spawn cmd, present its stdout as a path readable by the consumer. zshrs uses worker pool threads instead of fork — the FIFO/temp file is wired to a thread that runs the bytecode for cmd.
diff <(sort a.txt) <(sort b.txt)
# >(cmd) — process sub (output)
Spawn cmd, present its stdin as a path writable by the producer.
tar c . | tee >(gzip > backup.tgz) > backup.tar
# cmd N>&M — duplicate fd
Duplicate fd M to fd N. Common idiom: 2>&1 merges stderr into stdout. <&fd defaults the destination to fd 0; >&fd defaults to fd 1. Variable-expanded fd targets work: read line <&${COPROC[1]}.
find / 2>&1 | grep '\.zsh$' | head
coproc { echo from-child; }
read reply <&${COPROC[1]}
echo "$reply" # from-child
# cmd N<&- / cmd N>&- — close fd
Close fd N. POSIX form for explicitly tearing down a previously-opened fd (typically after exec FD< file).
exec 5< data.txt
read line <&5
exec 5<&- # close fd 5
# { cmds; } > file — block redirect
Apply a redirect to a compound command. WithRedirectsBegin/End bracket the inner ops so the parent shell's fds are restored after.
{
echo header
date
uname -a
} > report.txt
POSIX / Zsh Builtins
52 topics · the full POSIX-required set plus zsh's interactive-shell builtins. Every entry routes through fusevm CallBuiltin(id, argc); arrays splice via pop_args.
# cd [dir]
Change directory. With no argument, goes to $HOME. cd - jumps to $OLDPWD. Updates $PWD.
cd ~/repos/zshrs
cd - # back to previous
# pwd
Print working directory.
echo "now in $(pwd)"
# echo [args]
Print arguments space-separated with trailing newline. -n suppresses newline; -e enables backslash escapes.
echo hello world
echo -n "no newline"
echo -e "tab:\there"
# print
zsh's enhanced echo. -r raw, -n no newline, -l one arg per line, -z push to history.
print -l alpha beta gamma # one per line
print -P "%F{cyan}cyan%f" # prompt expansion
# printf format args…
C-style formatted output. Format directives: %s %d %f %x %o %b; backslash escapes \n \t etc.
printf "%-10s %5d\n" "items" 42
# export VAR=value
Mark a variable for export to subprocesses (writes to std::env).
export PATH="/opt/bin:$PATH"
export RUST_LOG=debug
# unset VAR…
Remove a variable. Removes from variables, arrays, assoc_arrays, and exported env.
unset DEBUG_FLAG
unset -f myfunc # remove function
# source file / . file
Execute a script in the current shell. zshrs's source path uses bytecode caching: first run compiles + stores to SQLite; subsequent runs deserialize cached chunks. Plugin delta cache replays state changes (functions, aliases, vars, hooks, zstyles, options) in microseconds.
source ~/.zshrc
. /etc/profile.d/lang.sh
# exit [n]
Exit the shell with status n (default last status). Aliases: bye, logout. Compile emits a forward jump patched past chunk-end (halts the VM). EXIT trap fires before halt.
[[ -z $1 ]] && { echo "missing arg"; exit 2; }
# true / false / :
Status helpers. true and : exit 0; false exits 1. : is also the no-op for argument expansion side effects.
: ${VAR:=default} # set if unset; expansion side effect, no command
while true; do …; done
# test EXPR / [ EXPR ]
POSIX conditional test. Use [[ … ]] for the zsh extended form (in-shell, no fork). Operators: -f -d -e -r -w -x -s -z -n = != < > -eq -ne -lt -le -gt -ge.
[ -f /etc/passwd ] && echo "exists"
[[ $count -gt 10 && -n $name ]]
# local VAR[=value]
Function-local scope. zshrs's local-scope semantics: declared vars are pushed onto a scope stack at function entry, popped at return. Assignment-without-declare-at-top-of-function is allowed (zsh idiom) but has caveats — see CLAUDE.md "Never use `local` inside loops".
my_func() {
local count=0
local result
…
}
# typeset / declare
Declare a variable with attributes. -a indexed array, -A assoc array, -i integer, -r readonly, -x exported, -l lowercase, -u uppercase, -g global (in func scope), -f function-scoped.
typeset -i count=0
typeset -A config
typeset -r APP_VERSION=1.0
declare -a names=(alice bob carol)
# readonly VAR=val
Mark a variable read-only. Subsequent assignments error.
readonly RELEASE_BRANCH=main
# integer VAR=expr
Declare an integer-typed variable. Subsequent assignments evaluate as arithmetic.
integer i=10
i=i+5
echo $i # 15
# float VAR=expr
Declare a float-typed variable.
float pi=3.14159
echo $((pi * 2))
# read VAR…
Read a line from stdin into one or more variables. Flags: -r raw (no backslash escapes), -n N N chars, -t SEC timeout, -s silent, -p prompt, -A arr read into array.
echo -n "name? "
read name
read -r line < /etc/hostname
read -A words <<< "alpha beta gamma"
# mapfile / readarray
Read each line of a file (or stdin) into successive elements of an array. Bash compat alias.
mapfile -t lines < /etc/hosts
echo "${#lines[@]} hosts"
# shift [n]
Remove the first n positional parameters (default 1).
process() {
cmd=$1; shift
do_thing "$cmd" "$@"
}
# eval string
Re-parse and execute the joined-args string as shell code. Single-quoted args defer expansion correctly (the lexer's \0-sentinel for single-quoted specials is honored by the compile path's trigger detection).
x=10
eval 'echo $x' # 10 (expansion deferred to eval-time)
eval "$(starship init zsh)"
# exec [cmd args]
Replace the current shell with cmd. With no command, applies redirections to the current shell (e.g. exec > log redirects subsequent output).
exec > build.log 2>&1
exec /usr/local/bin/newshell
# command [-pvV] cmd
Bypass functions/aliases and run the underlying command. -v prints how the name would resolve; -V verbose form.
command rm /tmp/junk # bypass user's `rm` function
command -v cargo # /Users/wizard/.cargo/bin/cargo
# builtin cmd
Force builtin dispatch (skip functions/aliases).
builtin cd /tmp # zshrs's cd, never a user override
# let "expr"
Evaluate arithmetic expression(s); exit 0 if last result is non-zero.
let "i = 1 + 2"
let "i++"
# set [-eux] [args…]
Set shell options or positional parameters. -e exit on error, -u nounset, -x trace, -o pipefail propagate pipeline status.
set -euo pipefail
set -- alpha beta gamma # set $1 $2 $3
# setopt / unsetopt
Toggle zsh-style options. Options live in executor.options; setopt foo turns on, unsetopt foo turns off.
setopt extended_glob
setopt null_glob
unsetopt nomatch
# shopt
Bash-compat option toggling. Maps onto the same options table as setopt.
shopt -s globstar
# emulate [-LR] [shell]
Switch emulation mode (zsh, sh, ksh, csh). -L local-to-function, -R reset all options to that shell's defaults.
emulate -L zsh # function-local zsh defaults
# getopts spec var
POSIX option parsing. Iterates flags from $@, sets $var to each option, $OPTARG to its value.
while getopts "vh:" opt; do
case $opt in
v) verbose=1 ;;
h) host=$OPTARG ;;
esac
done
# zparseopts
zsh's richer option parser. -A arr stores into an assoc array; supports long options.
zparseopts -A opts -- v h: c::
echo "${opts[-h]}"
# autoload [name…]
Mark a function for lazy loading from $fpath. zshrs's autoload path uses SQLite-cached bytecodes: first invocation deserializes a chunk; subsequent invocations skip lex/parse/compile entirely.
autoload -Uz compinit
compinit
# functions [name]
List defined functions, or print a function's body. +f prints names only.
functions # all functions
functions my_func # body of my_func
# unfunction name
Remove a function definition. Same as unset -f.
unfunction old_helper
# trap 'cmd' SIG
Install a signal handler. Special pseudo-signals: EXIT (run at shell exit), DEBUG (before each command), ERR (on non-zero status), ZERR (zsh-specific).
trap 'rm -rf $tmpdir' EXIT
trap 'echo got SIGINT' INT
# pushd / popd / dirs
Directory stack manipulation. pushd dir changes to dir and pushes onto the stack; popd pops and changes back. dirs -v lists.
pushd ~/work/proj
…
popd
# alias name='cmd'
Define a command alias. With no args lists all aliases. alias -g creates a global alias (substitutes anywhere on the command line).
alias ll='ls -lAh'
alias -g G='| grep'
# unalias [-m pattern] name
Remove an alias. -m matches by glob pattern.
unalias ll
unalias -m 'g*' # remove all aliases starting with g
# type / whence / where / which
Resolve a name to its source: alias, function, builtin, or external. whence -p shows path only; whence -a shows all matches.
type ls
whence -a cargo
which gcc
# hash / rehash / unhash
Command-name → path cache. rehash rebuilds the cache from $PATH (parallel scan across worker pool). unhash -dm 'pat' removes named-directory entries by glob.
rehash # after installing new binaries
hash # show cache
unhash -dm 'tmp*' # remove tmp* hashed dir refs
# ulimit / limit / unlimit
Resource limit query/set. -n open files, -s stack size, -u max processes, -c core dump.
ulimit -n 65536
ulimit -a # show all
# umask [mask]
Set/show file-creation mask.
umask 077 # owner-only by default
# times
Print user + system times for the shell and its children.
times
# caller
Print the line + filename of the caller of the current function. Useful in error reporters and debuggers.
my_assert() {
[[ $1 -eq $2 ]] || { echo "assertion failed at $(caller)"; exit 1; }
}
# help [name]
Print help for a builtin (or list builtins).
help echo
help
# enable / disable
Enable/disable a builtin by name. Useful for testing external-vs-builtin behavior.
disable cat # falls back to /bin/cat
enable cat # restore zshrs's cat builtin
# noglob cmd
Run cmd with glob expansion disabled for its arguments. Equivalent to setopt noglob; cmd; unsetopt noglob as a one-liner.
noglob find . -name '*.rs' # don't pre-glob *.rs
# ttyctl -f / ttyctl -u
Freeze / unfreeze the tty state. Used by completion code that reads stdin.
ttyctl -f # freeze
ttyctl -u # unfreeze
# sync
Force the OS to flush its buffer cache to disk. Direct sync(2) syscall.
cp big.iso /mnt/usb/
sync
# zmodload [-i] mod
Load a zsh module. zshrs's modules are statically linked, so zmodload is a no-op-and-succeed for compat (-i idempotent flag).
zmodload zsh/datetime
zmodload zsh/regex
# zsleep n
Subsecond-precision sleep. zsleep 0.5 sleeps 500ms. Different from coreutils sleep only in supporting fractional seconds reliably across implementations.
zsleep 0.25 # 250ms
# zsystem subcmd
zsh/system module operations. zsystem flock file for file locking; zsystem getflags for flag dumps.
zsystem flock /tmp/lockfile
# strftime fmt [time]
Format a unix timestamp using strftime(3) directives. Default time is now.
strftime "%Y-%m-%d %H:%M:%S"
# mkdir [-p] dirs…
Create directories. -p creates parents and doesn't fail if exists.
mkdir -p ~/.config/myapp/{cache,logs}
# vared VAR
Edit a variable's value via the line editor. Handy for interactive config edits.
vared PATH
# zformat -f var fmt args…
Sprintf with %X:value argument bindings; common in completion display formatting.
zformat -f line "%name (%size)" name:foo size:42
# zregexparse
State-machine regex parser used by completion and filename rewriting.
zregexparse vname tname regex
# zcompile file
Pre-compile a zsh script to .zwc. zshrs treats .zwc as one of its bytecode-cache inputs and deserializes the chunk directly when sourcing the original file.
zcompile ~/.zshrc
Anti-Fork Coreutils
23 topics · 2000-5000x faster than forking to /bin — every invocation runs in-process via direct syscalls.
# cat [files…]
Concatenate files to stdout. With no args, copies stdin. -n numbers lines; -A shows non-printable. No fork.
cat /etc/hosts
cat *.log | grep ERROR
# head [-n N] [files…]
First N lines (default 10). -c N first N bytes.
head -20 /var/log/system.log
# tail [-n N] [-f] [files…]
Last N lines. -f follow (in-process).
tail -f access.log
# wc [-lwc] [files…]
Line / word / char count.
wc -l *.rs # total Rust LOC
# sort [-n -r -u -k] [files…]
Sort lines. -n numeric, -r reverse, -u unique, -k N sort on key.
du -h * | sort -hr
# find path [predicates]
Walk directories applying predicates. -name, -type, -size, -mtime, -exec. In-process walk.
find . -name '*.rs' -size +1k
# uniq [-c -d -u] [files…]
Filter adjacent duplicates. -c prefix counts, -d show duplicates only.
cat history.log | sort | uniq -c | sort -nr
# cut [-d delim] -f fields
Extract fields from each line.
cut -d: -f1 /etc/passwd # all usernames
# tr SET1 SET2
Translate or delete characters.
echo HELLO | tr A-Z a-z
tr -d '\r' < crlf.txt > lf.txt
# seq [start] [step] end
Print a sequence of numbers.
for i in $(seq 1 10); do …; done
seq 0 0.5 5
# rev
Reverse each line character-wise.
echo hello | rev # olleh
# tee [-a] file…
Copy stdin to stdout AND to one or more files. -a appends.
build.sh 2>&1 | tee build.log
# basename path [suffix]
Strip directory and optionally a trailing suffix.
basename /tmp/file.txt # file.txt
basename /tmp/file.txt .txt # file
# dirname path
Strip the trailing component.
dirname /tmp/file.txt # /tmp
# touch [-a -m -t time] file…
Update file timestamps; create empty file if missing.
touch deploy.lock
# realpath path
Resolve symlinks and relative paths to a canonical absolute path.
realpath ./src/exec.rs
# sleep seconds
Pause for seconds (fractional supported).
sleep 0.5
# whoami
Effective username (direct geteuid + getpwuid syscalls).
echo "running as $(whoami)"
# id [-u -g -n] [user]
User/group ID info.
id -u # numeric uid
id -un # username
# hostname [-s -f]
System hostname. Direct syscall.
hostname # short
hostname -f # FQDN
# uname [-a -s -r -m]
Kernel name / release / arch. Direct uname(3).
uname -srm
# date [+fmt]
Print current date/time. +fmt uses strftime directives. Direct syscall.
date
date +%s
date "+%Y-%m-%d %H:%M:%S"
# mktemp [-d] [template]
Atomic temp file/dir creation.
tmp=$(mktemp)
tmpdir=$(mktemp -d)
Recently Closed Compat Gaps (eighty-eighth-pass)
46 constructs · all verified against ~/forkedRepos/zsh/Src/ via direct C-source ports. Each entry is pinned by at least one assertion in tests/zshrs_shell.rs; total: 968+ tests. These were the highest-impact gaps in man zshall coverage that remained after the seventy-seventh pass — most are param expansion, pattern matching, function scoping, and trap behavior.
Glob & Pattern
# extendedglob ~ exclusion at PATH level
Direct port of pattern.c P_EXCLUDE: matches RHS as a pattern against each LHS candidate's basename + full path, not as a separate CWD glob.
setopt extendedglob
echo /tmp/dir/*.txt~*README* # all .txt except README.txt
# /^pat at any path component
Negation operator now triggers in any path component, not just the leading word.
setopt extendedglob
echo /tmp/dir/^skipme # everything in /tmp/dir except files matching skipme
# $D/*, $D/(a|b) — glob meta after var ref
New BUILTIN_GLOB_EXPAND (id 343): pops a scalar pattern, runs expand_glob, pushes Value::Array. Compile path detects glob meta in literal segments only (so $?/$#/etc don't trigger).
D=/tmp/build
echo $D/* # globs after $D substitution
echo $D/(a|b) # alternation after $D substitution
# **/* recursive sort by full path
Recursive globs sort by full-path lex order (zsh's depth-first walk). Non-recursive globs keep basename-sorted output.
echo /tmp/dir/**/* # f, sub, sub/g — not f, g, sub
Parameter Expansion
# ${${a%.txt}#hel} — outer strip on inner result
Direct port of subst.c getarg machinery — same dispatch for inner and outer.
a=hello.txt
echo "${${a%.txt}#hel}" # lo
# ${(s. .)${(j. .)a}} — outer flag on inner expansion
Flag handler now recurses on leading ${ in rest; applies U/L/C/Split/Join/SplitWords/SplitLines to inner result.
a=(a b c)
echo "${(s. .)${(j. .)a}}" # a b c (split-then-rejoin)
# ${(flags)$(...)} — cmd-subst as flag operand
Branch added for cmd-subst operands. (P) indirect honored: captured output is treated as a variable name and looked up.
a=hi; echo "${(P)$(echo a)}" # hi (NOT "a")
echo "${(U)$(echo hello)}" # HELLO
echo "${(z)$(echo a b c)}" # a b c
# ${(U)${(s. .)s}[N]} — [N] subscript after inner expansion
Splits the flag-applied joined-scalar back to parts, indexes, re-applies case-transform flags.
s="x y z"
echo "${(U)${(s. .)s}[1]}" # X
# ${(l:N::s2:)val} — pad with empty s1 + s2 fallback
Pad parser collects both strings; when s1 empty and s2 given, s2 acts as fill char.
echo "${(l:5::0:)42}" # 00000 (zsh-faithful)
# ${(j[+])a}, ${(s[|])s} — bracket-pair flag delimiters
zsh subst.c get_strarg accepts matched bracket pairs as flag delimiters: [/], {/}, (/), </>. Both flag-parser sites updated.
a=(a b c)
echo "${(j[+])a}" # a+b+c
echo "${(j<X>)a}" # aXbXc
echo "${(s[|])\"x|y|z\"}" # x y z
# :Q modifier — backslash escape removal
hist.c remquote: strips paired '/" AND \X backslash escapes. Both :Q paths fixed.
a="a\\ b"
echo ${a:Q} # a b
# ${a//\X/repl} — backslash unescape for non-meta chars
Pattern pre-pass strips \X when X is not a glob meta. \?/\*/\[/\]/\(/\)/\|/\\ still escape.
a="x:y:z"
echo "${a//\:/-}" # x-y-z
echo "${a//\./X}" # works on dots too
# "${(o)a[@]}", "${(O)a[@]}", "${(n)a[@]}" — sort flags survive DQ when [@] given
Compile path encodes the at-subscript context through a new \u{03} sentinel in the flags string so the runtime DQ-stripper preserves array-only flags.
a=(c a b)
echo "${(o)a[@]}" # a b c
echo "${(O)a[@]}" # c b a
a=(10 2 1 22)
echo "${(n)a[@]}" # 1 2 10 22
# ${SECONDS-default} — zsh-special params always-set
Whitelist treats SECONDS, EPOCHSECONDS, EPOCHREALTIME, RANDOM, LINENO, HISTCMD, PPID, UID, EUID, GID, EGID, SHLVL as set even when not in self.variables. HISTCMD getter also added.
echo "${SECONDS-default}" # 0 (or current value)
echo "${UID-default}" # 501
echo "${HISTCMD-default}" # 0
Arrays & Subscripts
# Associative-array key insertion order
Storage switched from HashMap to indexmap::IndexMap so ${(k)h} / ${(kv)h} iterate in insertion order matching zsh's HashTable hnodes.
typeset -A h
h=(a 1 b 2 c 3)
echo ${(k)h} # a b c
echo ${(kv)h} # a 1 b 2 c 3
# for k v in arr — multi-name for loop
Direct port of parse.c par_for: parser collects all leading identifiers; compiler emits N-stride iteration with empty-string fill on short tail.
arr=(a 1 b 2 c 3)
for k v in $arr; do echo "$k=$v"; done
typeset -A h=(a 1 b 2)
for k v in ${(kv)h}; do echo "$k=$v"; done
# b=("${a[@]}") — array-to-array splice preserves boundaries
New scalar_assign_depth separate from assign_context_depth — only scalar RHS forces JOIN_STAR. Array init keeps splice.
a=("1 2" "3 4")
b=("${a[@]}")
echo ${#b} # 2 (preserves both elements)
# $a[@], $a[*] — bare (no-braces) splice
array_splice_ref extended to accept the no-braces form, identical semantics to ${a[@]}.
a=(x y z)
printf "%s\n" $a[@] # x, y, z (3 lines)
f() { echo $#; }; f $a[@] # 3
# b="${a[@]}" — scalar assignment joins via JOIN_STAR
Compile path forces BUILTIN_ARRAY_JOIN_STAR when scalar_assign_depth > 0 (zsh subst.c forces single-string for scalar RHS).
a=(1 2 3)
b="${a[@]}"
echo $b # 1 2 3
# a[$n]=() — variable subscript element-remove
Compile path now routes the subscript through compile_word_str when key has $ or backtick.
a=(1 2 3 4)
n=3
a[$n]=() # remove 3rd element
echo "${a[@]}" # 1 2 4
a=(1 2 3 4)
a[$#a]=() # remove last
echo "${a[@]}" # 1 2 3
# ${h[(I)pat]}, ${h[(R)pat]} on assoc — return ALL matches
(I)/(R) return all matching keys/values space-joined; (i)/(r) return first. (i)/(I) search keys; (r)/(R) search values.
typeset -A h=(a 1 b 2)
echo "${h[(I)*]}" # a b
echo "${h[(i)*]}" # a
echo "${h[(I)a]}" # a (single match)
typeset -A h=(a 1 b 1 c 2)
echo "${h[(R)1]}" # 1 1
# typeset -a a preserves existing array at top scope
Bare-declaration path now guards with in_function || !exists. typeset -aU dedupes in place.
a=(1 2 3); typeset -a a; echo $a # 1 2 3 (was empty)
a=(a b a c b); typeset -aU a; echo $a # a b c (was empty)
Arithmetic
# ((i=a[2])) — RHS array subscript pre-resolve
[ added to needs_eval trigger so MathEval (which runs pre_resolve_array_subscripts) handles it.
a=(10 20 30)
((i=a[2])); echo $i # 20
((sum=a[1]+a[2]+a[3])); echo $sum # 60
# ((h[a]++)), ((h[a]+=v)) — compound ops on assoc
Direct port of zsh's math.c LVAL_NUM_SUBSC: subscript receiver retains lvalue identity through compound operators.
typeset -A h
h[a]=5
((h[a]++)) # h[a] = 6
((h[a]+=10)) # h[a] = 16
# ((++a[i])), ((++h[k])) — pre-increment on subscript
New parse_subscript_arith_pre_inc + compile-side subscripted_arith_compound_check accepts the pre-op shape. Pre-op returns NEW value, post-op OLD.
a=(10 20 30)
((++a[2])); echo $a # 10 21 30
typeset -A h
h[a]=5
((++h[a])); echo $h[a] # 6
# ((a = cond ? T : F)) — ternary in assignment
? added to needs_eval triggers so MathEval handles ternary fully. ArithCompiler can't write back through ?:.
((a = 5 > 3 ? 99 : 0)); echo $a # 99
# abs/min/max/int/floor/ceil/trunc — int-preserving
Int-input return int (not 5.). Float input still returns float.
echo $((abs(-5))) # 5 (was 5.)
echo $((max(3,5,7))) # 7
echo $((abs(-5.5))) # 5.5 (still float)
Conditionals & Parser
# [[ a == a && (b == b || c == c) ]] — cond grouping parens
Lexer's incondpat resets on &&, ||, (, ), !, ]] per cond.c par_cond_3.
[[ a == a && (b == b || c == c) ]] && echo y
# case W in (P|Q)) BODY ;; — wrapped pattern with alternation
Parser accepts both bare (P) BODY and wrapped (P)) BODY (the (...) is the pattern wrapper, the second ) is the arm-close).
case foo in
(foo|bar)) echo y;;
(*)) echo n;;
esac
Functions & Scoping
# . file.sh ARG1 ARG2 — source passes positionals
Save outer positional_params, install args[1..] as new positionals, restore on exit.
# file.sh: echo "$1=$2"
. ./file.sh hi bye # hi=bye
set -- a b c
. ./file.sh inner # inside file.sh: $1=inner
echo "$@" # outside: a b c (preserved)
# typeset -A h=(...) in function shadows outer
New local_assoc_save_stack mirrors local_array_save_stack lifecycle. Both call_function paths (legacy + bytecode) updated.
typeset -A h=(a 1)
f() { typeset -A h=(b 2); echo $h[b]; }
f # 2
echo $h[a] # 1 (outer preserved)
echo "[${h[b]-empty}]" # [empty] (inner didn't leak)
# Subshell umask snapshot+restore
SubshellSnapshot snapshots libc::umask; subshell_end restores. zsh forks for (...) so umask dies with child; we run in-process so we manually reset.
umask 022
(umask 077) # subshell
umask # 022 (parent unchanged)
# Subshell EXIT trap fires at subshell end
Was firing at process exit. Now fires before parent continues. SubshellSnapshot snapshots+restores parent traps; inner trap fires with traps.remove("EXIT") so the inner execute_script doesn't recurse.
(trap "echo trapped" EXIT; true)
echo done # output: trapped\ndone (was: done\ntrapped)
Brace & Word Expansion
# {one,${a},three} — brace expansion after var substitution
Segment-concat path now emits BUILTIN_BRACE_EXPAND after concat when any literal segment contains { or }.
a=hi
echo {one,${a},three} # one hi three
echo pre{1,${a},2}post # pre1post prehipost pre2post
Builtins & I/O
# print -C N — column-padded output
Per-column width padding with 2-space separator (zsh format). Trailing partial rows skip padding after the last present item.
print -C 2 a b c d
# a c
# b d
print -C 2 alpha beta gamma delta
# alpha gamma
# beta delta
# read -e / read -E — echo line
zsh's bin_read calls fputs(buf, stdout) under both. -e echoes only (no assign); -E echoes AND assigns.
echo "abc" | { read -E v; echo "[$v]"; }
# abc
# [abc]
echo "abc" | { read -e v; echo "[$v]"; }
# abc
# []
# trap "..." ZERR / trap "..." ERR — fires on non-zero status
Wired into BUILTIN_ERREXIT_CHECK. Runs the trap body before the errexit decision; last_status saved/restored to prevent recursion on trap-body failures.
trap "echo zerr" ZERR
false
echo done
# zerr
# done
// generated against fusevm 0.12.1 dispatch · zshrs v0.10.1 · 15 chapters · 968+ tests · MenkeTechnologies