Skip to content

fix(scripts): the gates die on the bash macOS actually ships - #6

Merged
ruslan-korneev merged 1 commit into
masterfrom
bash32-portability
Jul 28, 2026
Merged

fix(scripts): the gates die on the bash macOS actually ships#6
ruslan-korneev merged 1 commit into
masterfrom
bash32-portability

Conversation

@ruslan-korneev

Copy link
Copy Markdown
Owner

Closes RUS-188.

The problem

macOS ships bash 3.2.57 as /bin/bash, and #!/usr/bin/env bash finds a newer one only when
Homebrew's is on PATH. Three core scripts used bash 4+ syntax:

Where Construct What actually happens on 3.2
gate.sh ${have,,} / ${want,,} bad substitutionfatal in a non-interactive shell. gate.sh workspace with INTAKE_WRITEBACK=true aborts. Not a failing gate: no gate at all.
intake.sh ${kind^^} same, in the writeback path
review.sh mapfile not a command → array unset → next line reads ${engines[$idx]} under set -u and aborts. A review that dies before spawning a reviewer.

bash -n is clean on all three, which is why this never showed up until something ran.

The fix

lib.sh gains lower/upper (tr-based) — case folding is needed in two scripts now — and review.sh
reads the engine lists with while IFS= read -r.

This closes the last known gap in the suite: check_xfail 'core scripts avoid bash-4-only constructs'
is promoted to check. A new assertion exercises lower, upper and the array read under
/bin/bash itself
rather than trusting the replacements.

The detector was broken too

It shared a comment-and-quote stripper with the GNU-tool rule. But "${have,,}" sits inside double
quotes nearly every time it appears, so stripping quoted strings blinded the check to exactly what it
was looking for. Split in two:

  • comments only → bash-4 expansions
  • comments and quoted strings → tool names, since naming a binary in a warning is not a use of it

How to verify

bash tests/run.sh                            # 145 passed, 0 failed, 0 known gaps
ADW_TEST_SHELL=/bin/bash bash tests/run.sh
/bin/bash --version | head -1                # 3.2.57 on macOS

Mutation, one construct at a time — each fails exactly one assertion:

python3 - <<'PY'
s=open('scripts/gate.sh').read()
open('scripts/gate.sh','w').write(s.replace('"$(lower "$have")" != "$(lower "$want")"','"${have,,}" != "${want,,}"',1))
PY
bash tests/run.sh portability     # 1 FAIL
git checkout scripts/gate.sh

Same for ${kind^^} in intake.sh, mapfile in review.sh, and breaking lower().

Worth knowing: the first attempt reported zero failures for two of them — the perl one-liners
doing the mutation had silently not applied, so that run proved nothing. The mutation step now fails
loudly when its own edit does not match, and that is the only reason the quote-stripping blind spot
above was found rather than shipped.

Not verified

  • The two runtime paths that were actually dying — gate.sh workspace with INTAKE_WRITEBACK=true, and
    review.sh past engine selection — still are not covered end to end: one needs a tracker, the other
    needs a live engine CLI. What is covered is that the constructs are gone and that their replacements
    behave correctly under 3.2.
  • Pre-existing and not touched here: if engines.sh pick-review ever returns nothing,
    ${engines[$idx]} is still an unbound-variable abort under set -u. Independent of the mapfile
    change — it read the same way before. Not filed yet; say the word and I will.

Three core scripts used bash 4+ syntax. macOS ships bash 3.2.57 as /bin/bash, and
`#!/usr/bin/env bash` finds a newer one only when Homebrew's is on PATH — not on a stock machine, and
not on a macOS CI runner. Every script parses fine under 3.2 (`bash -n` is clean), so this only
appeared at runtime, mid-run, on the operator's laptop:

- gate.sh `${have,,}` / `${want,,}` — bad substitution is FATAL in a non-interactive shell, so
  `gate.sh workspace` with INTAKE_WRITEBACK=true aborted instead of returning a verdict. Not a failing
  gate: no gate at all.
- intake.sh `${kind^^}` — same, in the writeback path.
- review.sh `mapfile` — not a command on 3.2, so the array stayed unset and the next line read
  `${engines[$idx]}` under `set -u` and aborted. A review that dies before spawning a reviewer.

lib.sh gains `lower`/`upper` (tr-based) since case folding is now needed in two scripts, and review.sh
reads the engine lists with `while IFS= read -r`.

Closes the last known gap in the suite: `check_xfail 'core scripts avoid bash-4-only constructs'` is
promoted to `check`, and a new assertion exercises `lower`, `upper` and the array read under
/bin/bash itself rather than trusting the replacements.

The detector needed fixing too. It shared a comment-and-quote stripper with the GNU-tool rule, but
`"${have,,}"` sits inside double quotes nearly every time it appears, so stripping quoted strings
blinded it to exactly what it looks for. Split into two: comments only for bash-4 expansions, comments
and quoted strings for tool names (a binary named in a warning message is not a use of it).

Verified by mutation, one construct at a time: restoring `${have,,}` in gate.sh, `${kind^^}` in
intake.sh, `mapfile` in review.sh, or breaking `lower()` each fails exactly one assertion. The first
attempt at this reported zero failures for two of them — the perl one-liners had silently not applied.
The mutation runner now fails loudly when its own edit does not match, which is the only reason the
quote-stripping blind spot above was found rather than shipped.

Suite: 145 passed, 0 failed, 0 known gaps, under bash 5.2 and 3.2.

VERSION 2.3.0 → 2.3.1.

Refs RUS-188
@ruslan-korneev
ruslan-korneev merged commit c0eb75c into master Jul 28, 2026
2 checks passed
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.

1 participant