Part of #761.
Problem
The assertion layer still forks avoidable subshells on its hottest paths (measured on macOS bash 3.2, Apple Silicon, commit 1ae3e82):
assert_equals/assert_not_equals fork twice per call, even on success. src/assert.sh:159,161 and src/assert.sh:182,184 capture bashunit::str::strip_ansi with $(...). strip_ansi (src/str.sh:4) already has a pure-bash fast path for plain input, but callers still pay the command-substitution fork to read it. Measured: a file with 1000 assert_equals runs in ~1.45s vs ~0.68s for the trivial baseline, i.e. ~0.77ms extra per assertion.
- Failure-path label resolution forks 2-3 more times per failing assertion.
bashunit::assert::label (src/assert.sh:15) captures find_test_function_name via $(...) and is itself always captured via $(...) at every call site; bashunit::handle_bool_assertion_failure (src/assert.sh:120) and bashunit::fail (src/assert.sh:26) repeat the same nested-capture pattern.
bashunit::run_command_or_eval forks a pipeline per non-literal assert_true/assert_false. src/assert.sh:111 runs $(command -v "$cmd" | "$GREP" -cE '^alias') just to detect an alias.
Task — checklist, one commit each
Tests first (TDD)
Behavior must not change; the existing suite is the spec. Add a regression test for each new return-slot helper: call it with one of its own internal variable names (dynamic-scoping trap documented in .claude/rules/bash-style.md, bug precedent PR #672).
Constraints
- Bash 3.0+ only: no
declare -A, no [[ ]], no ${var,,}, no printf -v (3.1+).
- Console output must remain byte-identical (snapshot/acceptance tests will catch drift).
Acceptance criteria
Line references valid at commit 1ae3e82.
Part of #761.
Problem
The assertion layer still forks avoidable subshells on its hottest paths (measured on macOS bash 3.2, Apple Silicon, commit 1ae3e82):
assert_equals/assert_not_equalsfork twice per call, even on success.src/assert.sh:159,161andsrc/assert.sh:182,184capturebashunit::str::strip_ansiwith$(...).strip_ansi(src/str.sh:4) already has a pure-bash fast path for plain input, but callers still pay the command-substitution fork to read it. Measured: a file with 1000assert_equalsruns in ~1.45s vs ~0.68s for the trivial baseline, i.e. ~0.77ms extra per assertion.bashunit::assert::label(src/assert.sh:15) capturesfind_test_function_namevia$(...)and is itself always captured via$(...)at every call site;bashunit::handle_bool_assertion_failure(src/assert.sh:120) andbashunit::fail(src/assert.sh:26) repeat the same nested-capture pattern.bashunit::run_command_or_evalforks a pipeline per non-literalassert_true/assert_false.src/assert.sh:111runs$(command -v "$cmd" | "$GREP" -cE '^alias')just to detect an alias.Task — checklist, one commit each
strip_ansi(e.g.bashunit::str::strip_ansi_to_slotwriting_BASHUNIT_STR_STRIPPED_OUT, per.claude/rules/bash-style.md), whose no-ANSI fast path sets the global with zero forks. Convertassert_equalsandassert_not_equalsto it. Keep the echoingstrip_ansias a thin wrapper for any other callers.bashunit::assert::label,bashunit::handle_bool_assertion_failure, andbashunit::failto return slots: add slot variants ofbashunit::helper::find_test_function_name(src/helpers.sh:14) andbashunit::helper::normalize_test_function_name(src/helpers.sh:40), then update alllabel="$(bashunit::assert::label ...)"call sites insrc/assert.sh.src/assert.sh:111with a fork-free builtin check, e.g.if alias -- "$cmd" >/dev/null 2>&1; then eval .... Verify equivalence: for functions/binaries/nonexistent commandsaliasexits non-zero, matching the currentcommand -v | grep -c '^alias'result. Cover with a unit test that defines an alias (requiresshopt -s expand_aliasesin the test) and one that uses a function.Tests first (TDD)
Behavior must not change; the existing suite is the spec. Add a regression test for each new return-slot helper: call it with one of its own internal variable names (dynamic-scoping trap documented in
.claude/rules/bash-style.md, bug precedent PR #672).Constraints
declare -A, no[[ ]], no${var,,}, noprintf -v(3.1+).Acceptance criteria
./bashunit tests/and./bashunit --parallel tests/pass after every commit.make sa,make lintpass;shfmt -w .produces no diff.assert_equalsfile in the PR (baseline above).Line references valid at commit 1ae3e82.