Skip to content

fix(hack): guard empty-array expansion in lint-drift for bash 3.2#117

Open
omergk28 wants to merge 1 commit into
ActiveMemory:mainfrom
omergk28:fix/lint-drift-bash32-empty-array
Open

fix(hack): guard empty-array expansion in lint-drift for bash 3.2#117
omergk28 wants to merge 1 commit into
ActiveMemory:mainfrom
omergk28:fix/lint-drift-bash32-empty-array

Conversation

@omergk28

Copy link
Copy Markdown
Contributor

Summary

make audit — the contributing guide's mandatory pre-PR gate — aborts on stock macOS before running a single drift check:

./hack/lint-drift.sh: line 39: exclude_args[@]: unbound variable

bash 3.2 (the newest Apple ships, frozen at the GPLv2 boundary) treats "${arr[@]}" on an empty array as an unbound variable under set -u; bash 4.4+ does not. drift_grep expands its exclude_args array unconditionally and several call sites pass no excludes, so every macOS contributor hits this on the very first audit.

Fix

Guard the expansion with the canonical parameter-expansion alternate form ${arr[@]+"${arr[@]}"}: an empty array expands to zero words, a populated array reproduces the original quoted expansion, and bash 4+ behavior is unchanged. The other hack/ array expansions were checked empirically — all safe today (count-guarded or hardcoded non-empty); details in the spec.

Verification

  • Reproduced the abort on bash 3.2.57 (stock macOS) before the fix; bash hack/lint-drift.sh now exits 0.
  • go build ./... clean.
  • Spec: specs/fix-lint-drift-bash32-empty-array.md; the gotcha is captured in LEARNINGS.md so the next session doesn't re-debug it.

Provenance

Split out of #113 (the #93 TOCTOU fix) so each PR carries a single concern, per the contributing guide. This is an independent commit touching disjoint files (hack/lint-drift.sh, its spec, and a LEARNINGS entry).

🤖 Generated with Claude Code

@josealekhine

Copy link
Copy Markdown
Member

1. Spec miscount

specs/fix-lint-drift-bash32-empty-array.md says the no-exclude drift_grep call sites are "checks 2, 3, and 8":

Several drift_grep call sites pass no exclude globs (checks 2, 3, and 8) [...]

Check 8 (strings.Join) uses a direct grep, not drift_grep, so it's unaffected — only checks 2 and 3 trip this. Trivial doc slip.

2. The shellcheck claim isn't actually exercised

The PR's verification implies make audit covers the change, but hack/lint-shellcheck.sh explicitly excludes hack/ scripts:

# hack/lint-shellcheck.sh (header)
# `hack/` scripts are out of scope for now (they run only on developer machines).

And CI (.github/workflows/ci.yml) only runs lint-shellcheck / lint-powershell / golangci-lint / eslint — not lint-drift or audit. So:

  • "make audit passes" doesn't lint the changed file, and
  • no automated gate guards this fix — CI is Linux/bash 5, which can't reproduce a 3.2-only bug anyway.

The guarded form is the standard idiom and almost certainly shellcheck-clean, but since the repo's own gate skips it, consider running shellcheck hack/lint-drift.sh once by hand. The inline comment + LEARNINGS entry are a reasonable mitigation for a dev-only script.

3. The out-of-scope audit is slightly optimistic

The "Out of Scope" section's conclusion (other sites don't fail in practice) is right, but the enumeration isn't exhaustive:

  • detect-ai-typography.sh EXTS isn't a "hardcoded non-empty literal" — it's parsed from a defaulted variable, so --ext "" would empty it and abort at the element expansion on bash 3.2:

    # hack/detect-ai-typography.sh:50-51
    IFS=',' read -ra EXTS <<< "$EXT"
    for ext in "${EXTS[@]}"; do
  • hack/lint-powershell.sh:100 expands "${TARGETS[@]}" with the same count-guard-first pattern as lint-shellcheck.sh, but wasn't listed in the audit.

Both are correctly latent-only (safe today), just worth knowing the list isn't complete if anyone does the follow-up sweep.


Verdict

A textbook small fix — correct, scoped, documented, with the bug captured as a reusable learning. The nits are cosmetic.

Great job @omergk28 💯 .

Stock macOS ships bash 3.2 (GPLv2 freeze), which treats
"${arr[@]}" on an empty array as an unbound variable under
set -u — bash 4.4+ does not. drift_grep expands its exclude
array unconditionally and several call sites pass no excludes,
so make lint-style (and therefore make audit, the mandatory
pre-PR gate) aborted on every stock Mac before running a single
drift check.

Guard with the parameter-expansion alternate form,
${arr[@]+"${arr[@]}"}: empty expands to zero words, populated
reproduces the original quoted expansion, bash 4+ behavior
unchanged. The other hack/ array expansions are safe today
(count-guarded, defaulted, or hardcoded non-empty); details in
the spec.

Captures the gotcha in LEARNINGS.md so the next session does
not re-debug it.

Spec corrections per ActiveMemory#117 review (josealekhine): the no-exclude
drift_grep sites are checks 2 and 3 only (check 8 uses a direct
grep); the verification section now states that no CI/make gate
lints hack/ scripts (lint-shellcheck excludes them) rather than
implying make audit covers the change; and the out-of-scope
array-expansion audit now characterises each site accurately
(lint-powershell is count-guarded like lint-shellcheck;
detect-ai-typography's EXTS is defaulted via ${EXT:-md}, not a
hardcoded literal).

Spec: specs/fix-lint-drift-bash32-empty-array.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Omer Kocaoglu <omergk28@gmail.com>
@omergk28 omergk28 force-pushed the fix/lint-drift-bash32-empty-array branch from c8aa7b0 to 25149af Compare June 28, 2026 03:10
@omergk28

Copy link
Copy Markdown
Contributor Author

Thanks — all three nits fixed, and rebased onto current main (the LEARNINGS.md conflict is resolved), in 25149af6:

  • F1 — miscount. Spec now says the no-exclude drift_grep sites are checks 2 and 3 only, and explicitly notes check 8 (strings.Join) uses a direct grep, not drift_grep.
  • F2 — verification overclaim. Spec now states plainly that no CI/make gate lints hack/ scripts (lint-shellcheck excludes them; CI runs lint-shellcheck but not lint-drift/audit), and points at manual shellcheck hack/lint-drift.sh. The concrete verification is now the bash-3.2.57 before/after (exclude_args[@]: unbound variable abort → lint-drift: clean, exit 0).
  • F3 — enumeration. Now characterizes each site accurately: lint-shellcheck/lint-powershell are count-guarded (${#TARGETS[@]} -eq 0); build-all.sh is a hardcoded literal; detect-ai-typography.sh's EXTS is defaulted via ${EXT:-md} (so even --ext "" collapses to md), not a literal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants