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 call300-assertion run
two tr forks12.8 ms1419 ms
nocasematch0.087 ms131 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.
@ChemaclassChemaclass added the enhancement New feature or request label Aug 2, 2026
@ChemaclassChemaclass 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 mainAug 2, 2026
37 checks passed
@Chemaclass
Chemaclass deleted the perf/ignore-case-nocasematch branch August 2, 2026 17:15
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancementNew feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Chemaclass
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
perf(assert): fold case with nocasematch instead of two tr forks by Chemaclass · Pull Request #977 · TypedDevs/bashunit · GitHub
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 call300-assertion run
two tr forks12.8 ms1419 ms
nocasematch0.087 ms131 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.
@ChemaclassChemaclass added the enhancement New feature or request label Aug 2, 2026
@ChemaclassChemaclass 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 mainAug 2, 2026
37 checks passed
@Chemaclass
Chemaclass deleted the perf/ignore-case-nocasematch branch August 2, 2026 17:15
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancementNew feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Chemaclass
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' perf(assert): fold case with nocasematch instead of two tr forks by Chemaclass · Pull Request #977 · TypedDevs/bashunit · GitHub
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 call300-assertion run
two tr forks12.8 ms1419 ms
nocasematch0.087 ms131 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.
@ChemaclassChemaclass added the enhancement New feature or request label Aug 2, 2026
@ChemaclassChemaclass 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 mainAug 2, 2026
37 checks passed
@Chemaclass
Chemaclass deleted the perf/ignore-case-nocasematch branch August 2, 2026 17:15
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancementNew feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Chemaclass
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' perf(assert): fold case with nocasematch instead of two tr forks by Chemaclass · Pull Request #977 · TypedDevs/bashunit · GitHub
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 call300-assertion run
two tr forks12.8 ms1419 ms
nocasematch0.087 ms131 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.
@ChemaclassChemaclass added the enhancement New feature or request label Aug 2, 2026
@ChemaclassChemaclass 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 mainAug 2, 2026
37 checks passed
@Chemaclass
Chemaclass deleted the perf/ignore-case-nocasematch branch August 2, 2026 17:15
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancementNew feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Chemaclass
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' perf(assert): fold case with nocasematch instead of two tr forks by Chemaclass · Pull Request #977 · TypedDevs/bashunit · GitHub
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 call300-assertion run
two tr forks12.8 ms1419 ms
nocasematch0.087 ms131 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.
@ChemaclassChemaclass added the enhancement New feature or request label Aug 2, 2026
@ChemaclassChemaclass 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 mainAug 2, 2026
37 checks passed
@Chemaclass
Chemaclass deleted the perf/ignore-case-nocasematch branch August 2, 2026 17:15
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancementNew feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Chemaclass
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' perf(assert): fold case with nocasematch instead of two tr forks by Chemaclass · Pull Request #977 · TypedDevs/bashunit · GitHub
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 call300-assertion run
two tr forks12.8 ms1419 ms
nocasematch0.087 ms131 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.
@ChemaclassChemaclass added the enhancement New feature or request label Aug 2, 2026
@ChemaclassChemaclass 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 mainAug 2, 2026
37 checks passed
@Chemaclass
Chemaclass deleted the perf/ignore-case-nocasematch branch August 2, 2026 17:15
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancementNew feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Chemaclass
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); perf(assert): fold case with nocasematch instead of two tr forks by Chemaclass · Pull Request #977 · TypedDevs/bashunit · GitHub
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 call300-assertion run
two tr forks12.8 ms1419 ms
nocasematch0.087 ms131 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.
@ChemaclassChemaclass added the enhancement New feature or request label Aug 2, 2026
@ChemaclassChemaclass 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 mainAug 2, 2026
37 checks passed
@Chemaclass
Chemaclass deleted the perf/ignore-case-nocasematch branch August 2, 2026 17:15
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancementNew feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Chemaclass