Skip to content

perf(assert): fold case with nocasematch instead of two tr forks - #977

Merged
Chemaclass merged 3 commits into
mainfrom
perf/ignore-case-nocasematch
Aug 2, 2026
Merged

perf(assert): fold case with nocasematch instead of two tr forks#977
Chemaclass merged 3 commits into
mainfrom
perf/ignore-case-nocasematch

Conversation

@Chemaclass

Copy link
Copy Markdown
Member

🤔 Background

assert_contains_ignore_case lowercased both operands through a tr pipeline, paying two subprocess forks on every call. shopt -s nocasematch folds case inside the case statement itself and costs none.

per call 300-assertion run
two tr forks 12.8 ms 1419 ms
nocasematch 0.087 ms 131 ms

💡 Changes

nocasematch arrived in Bash 3.1 and this project's floor is 3.0, so the tr path stays as the fallback for that one version — gated by a predicate in the same shape as runner/context.sh's existing pipefail check. Verified by forcing the predicate to fail: the whole assert suite passes on the 3.0 path with identical results.

The option is saved and restored, not blindly unset — it's global, and a user's test file may have set it deliberately. shopt -q is a builtin, so the save is free.

🚫 Two better-looking approaches, both rejected

A pure-bash A-Z loop would be fork-free on every version including 3.0, and benchmarks well (0.22ms vs 2.50ms at 8 chars, crossing over around 128). It's still wrong. In a UTF-8 locale tr '[:upper:]' '[:lower:]' folds accented text, so ñü matches ÑÜ today; an ASCII-only loop would silently stop matching it — and this repo's Spanish, Brazilian and Japanese locale jobs exist for exactly that. nocasematch folds non-ASCII identically, checked in both directions.

${var,,} is equally correct on Bash 4+, and was rejected for a different reason: it's banned syntax here, so using it would mean punching an exception into the Bash 3.0 compatibility gate — the strongest contract in the repo. nocasematch needs no exception and starts helping a whole minor version earlier.

✅ Verification

Three new tests pin the behaviour: unset stays unset, set stays set, non-ASCII still folds.

make sa · make lint · bash build.sh bin -v✅ Build verified ✅ · 1657 sequential / 1616 parallel-simple-strict · fork budgets unchanged (11/11).

assert_contains_ignore_case lowercased both operands through a `tr` pipeline,
paying two subprocess forks on every call. `shopt -s nocasematch` folds case
inside the `case` statement itself and costs none. Measured on Bash 3.2: 0.087ms
per call against 12.8ms, and 131ms against 1419ms for a 300-assertion run.

nocasematch arrived in Bash 3.1 and this project's floor is 3.0, so the tr path
stays as the fallback for that one version -- gated by a predicate in the same
shape as runner/context.sh's existing pipefail check. Verified by forcing the
predicate to fail: the whole assert suite passes on the 3.0 path, with identical
results.

Two things worth recording, because both rule out approaches that look better.

A pure-bash A-Z loop would be fork-free on every version including 3.0, and it
benchmarks well: 0.22ms against 2.50ms at 8 characters, crossing over around
128. It is still wrong. In a UTF-8 locale `tr '[:upper:]' '[:lower:]'` folds
accented text, so `ñü` matches `ÑÜ` today; an ASCII-only loop would silently
stop matching it, and the repo's Spanish, Brazilian and Japanese locale jobs
exist for exactly this. nocasematch folds non-ASCII identically -- checked in
both directions.

`${var,,}` is equally correct on Bash 4+ and was rejected for a different
reason: it is banned syntax here, so using it would mean punching an exception
into the Bash 3.0 compatibility gate, which is the strongest contract in the
repo. nocasematch needs no exception and starts helping a whole minor version
earlier.

The option is saved and restored rather than blindly unset -- it is global, and
a user's test file may have set it deliberately. `shopt -q` is a builtin, so the
save is free. Three tests pin that: unset stays unset, set stays set, and
non-ASCII still folds.

1657 sequential / 1616 parallel; baseline + 3. Fork budgets unchanged.
@Chemaclass Chemaclass added the enhancement New feature or request label Aug 2, 2026
@Chemaclass Chemaclass self-assigned this Aug 2, 2026
The new non-ASCII test failed on Windows. Not a regression: Git Bash runs in a
C-ish locale where neither implementation folds accented case. Verified both
agree there -- under LC_ALL=C, tr leaves 'ÑÜ' unchanged and nocasematch does not
match either, which is correct behaviour and identical between the two paths.

The test was asserting a UTF-8-only outcome as if it were universal. It now
probes whether the platform folds non-ASCII at all and skips when it does not,
so it still guards the equivalence that matters wherever that equivalence is
observable.
The Bash 3.0 jobs went red on the new leak tests, and correctly so: they call
`shopt -u nocasematch` directly, and on 3.0 that is an "invalid shell option
name" error, so the tests could not even set themselves up.

The production code was already gated -- 3.0 takes the tr path. Only the tests
reached for the option unconditionally. They now skip on the same predicate,
which is also the honest outcome: on 3.0 there is no nocasematch, so there is no
option to leak.
@Chemaclass
Chemaclass merged commit ea5659a into main Aug 2, 2026
37 checks passed
@Chemaclass
Chemaclass deleted the perf/ignore-case-nocasematch branch August 2, 2026 17:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant