Contributing
July 26, 2026 · View on GitHub
Contributions are welcome — issues, suggestions, and pull requests. This guide covers the coding conventions and the test suite. For how the code is organised, see docs/Architecture.md.
Coding conventions
ell is deliberately dependency-light bash. A few conventions keep it portable, fast, and consistent; please follow them:
- Portability: bash >= 4.1, GNU and BSD tools. The code runs on Linux and
macOS and under
set -o posixin the test harness. Avoid bashisms that break on 4.1 (e.g. process substitution< <(...)underset -o posix— use a here-string instead), and prefer POSIX-portable tool invocations (statalready falls back from GNU to BSD form). - No per-line subprocesses on hot paths. Streaming loops and logging must
not fork
grep/cut/tr/sed/date/basenameper line/chunk; use bash builtins (parameter expansion,[[ ]],printf -v,printf '%(...)T'). - Prefer builtins over external tools generally, and keep the runtime dependency surface small (bash, curl, awk).
- Quoting and comparisons. Quote expansions. Use the portable
[ "x${VAR}" = "xVALUE" ]idiom for string comparisons (thexprefix guards against values that look like operators); reserve[[ ]]for where it is actually needed (regex=~, glob==). Use[ "${n}" -ge N ]for numbers. - Style. Trailing semicolons on statements, as in the existing code. Keep
functions small; give private helpers an
_ell_/_json_-style prefix and export public functions withexport -f. - Security first. Never
evaluser-influenced data. Treat templates, plugins and config as the trust-sensitive surfaces they are (see docs/Risk_Consideration.md). Keep secrets out of argv and logs. - Run ShellCheck.
error-level findings block CI; keep the diff clean of new warnings too.
Every behaviour-changing PR should come with tests, and the full suite must pass under both supported bash versions (below).
Testing
The tests are self-checking Bash scripts: each asserts expected values and
exits non-zero on any failure, so they can gate CI without human inspection.
They use a tiny built-in assertion helper (tests/assert.sh) rather than an
external framework, keeping the project dependency-free. The LLM backends are
exercised offline via the ell_echo dummy backend and file:// JSON fixtures,
so no network or API key is required.
Layout
Tests come in two flavours:
-
Unit tests live next to the source they cover, named
<source>.test.sh, so a file's tests are easy to find right beside it:helpers/json.sh helpers/json.test.sh helpers/logging.sh helpers/logging.test.sh helpers/piping.sh helpers/piping.test.sh helpers/render_to_text.awk helpers/render_to_text.test.sh plugins/redaction/50_post_input.sh plugins/redaction/50_post_input.test.sh -
End-to-end tests that drive the whole
ellpipeline (and their JSON fixtures) live intests/, alongside the shared assertion helpertests/assert.sh.
Running
Run the whole suite once, on your host:
bash tests/entry.sh
Or run it against the oldest and current supported Bash versions in Docker:
bash tests/docker.sh
tests/entry.sh auto-discovers every *.test.sh in the repository and then
runs the end-to-end tests. docker.sh runs it inside bash:4.1 and bash:5.2
containers and fails if the suite fails under either version.
Continuous integration
.github/workflows/ci.yml runs on every push and pull request:
- ShellCheck over all shell scripts, run at
-S warning. Botherrorandwarningfindings block the build: the repo is clean at warning severity, so the gate ratchets the warning count at zero to prevent warnings from silently accumulating. Unavoidable findings are silenced locally with a justified# shellcheck disable=SCxxxxdirective rather than by lowering the gate. - Tests across a
bash:4.1andbash:5.2container matrix. - Tests (mawk) with mawk as the default
awk, exercising the portablerender_to_text.awk. - Tests (macOS / BSD tools) on
macos-latest, exercising the BSD toolchain (BSD awk/sed/stat/mktemp, macOS curl). - Tests (Windows / Git Bash, informational) on
windows-latest, which iscontinue-on-errorand never blocks the build; it reports what works under Git Bash rather than gating on it.
Adding a test
For a unit test, create <source>.test.sh next to the file it covers, source
the assertion helper (via a path relative to that location), write assertions,
and end with assert_summary. It is picked up automatically by entry.sh —
no registration needed:
#!/usr/bin/env bash
set -o posix;
DIR="$(dirname "${0}")";
# From helpers/ this is ../tests/assert.sh; adjust the depth for other dirs.
. "${DIR}/../tests/assert.sh";
. "${DIR}/my_helper.sh";
assert_equals "adds up" "3" "$((1 + 2))";
assert_summary;
End-to-end tests that need JSON fixtures or the full pipeline go in tests/
(sourcing "${DIR}/assert.sh") and are registered with an explicit
run_test tests/<name>.sh line in tests/entry.sh.