The zsh I source at the top of every script — flags, logging, dry-run, parallelism, and locking — so the script itself can be five lines of real work. It's the boilerplate I always reach for, factored out of my dotfiles.
-
argparse/— a composable argument parser. Source one small file per capability, in order, and a script gets-h/--help(auto-generated from whichever modules it sourced),-n/--dry-run, repeatable-q/-vverbosity, and-y/--yes:source /path/to/the-usual/argparse/_init.zsh # expands -abc to -a -b -c source /path/to/the-usual/argparse/n.zsh # -n (dry run) source /path/to/the-usual/argparse/qv.zsh # -q/-v + the log_* family source /path/to/the-usual/argparse/_h.zsh # -h (help) — must be last
(
utils.zshandlog.zshare the shared dependencies; each module pulls in what it needs relative to its own location, so you only source the entry files above.qv.zshlayers-q/-von top of thelog_*family.) -
log.zsh— the severity-colored, script-name-prefixed, stderr-boundlog_*family (success/info/warning/error/fatal, each with verbosity-gated_v/_vvvariants) plusmkdir_v/mv_vwrappers. Source it directly for logging without the-q/-vflag parsing. -
concurrency.zsh—wait_if_too_many_jobs, a bounded job pool that caps background jobs at twice the CPU count. -
mutex.zsh—mutex/try_mutex, a lock held by a coprocess so it releases on exit even on Ctrl-C, leaving no orphaned lock. -
coreutils.zsh— fork-free file/time helpers backed by zsh builtins differences (file_size, etc.). -
debug.zsh—inspect, a one-call dump of a variable, array, or associative array.
argparse/n.zsh adds -n/--dry-run and sets MODE_DRY_RUN=1 — but it can't
know which lines are destructive, so guard the side effects yourself:
if [[ -n $MODE_DRY_RUN ]]; then
log_info "rm $f"
else
rm "$f"
fiCheck it with [[ -n $MODE_DRY_RUN ]] ([[ -z ]] for the wet path) — the flag
is set-or-unset, and this matches both how n.zsh sets it and how log_*
reads it. Don't use (( MODE_DRY_RUN )): it evaluates the value as
arithmetic, and the variable is exported precisely so that child scripts and
humans can set it by hand — a natural MODE_DRY_RUN=true reads as false
there (zsh resolves true as an unset variable name), so the guarded commands
run wet while log_* still prefixes every line [DRY_RUN]. Log the bare
command, and a dry run reads back as a labeled transcript of what a real run
would do — no extra wiring.
just test # run the whole suite
zsh test/test-log.zsh # or run one file directlyTests live in test/; the test/manual/ scripts are fixtures the automated
tests invoke (and can also be run by hand). Each test locates the library
relative to its own path, so the suite runs from anywhere.
Lifted out of my dotfiles, where it lives as a submodule. The pieces are now
path-independent: each file resolves its siblings relative to its own location
(via ${${(%):-%x}:A:h}), so you can drop the checkout anywhere and source any
single file by its path — it pulls in what it needs. The concurrency and mutex
helpers source log.zsh themselves, so they no longer assume the argparse
flags were sourced first.