fix(lint): avoid bash 4 associative arrays in check-vale-style-sync.sh

check-vale-style-sync.sh used `declare -A` for a per-skill regex cache.
Associative arrays are bash 4.0+; this script runs as an always-run
pre-push hook with `language: system`, so it inherits whatever bash is
first on the invoking user's PATH. On macOS's stock bash 3.2, `declare -A`
at top level aborts immediately under `set -euo pipefail` — every push
would hard-fail before the sync check ran anything.

Replaced with two parallel indexed arrays (HOOK_REGEX_CACHE_KEYS/_VALS),
linear-scanned by index — same caching behavior (avoids re-parsing both
pre-commit manifests when agent-audit is probed twice), but only ever
uses ${#arr[@]} and index access, never a bare ${arr[@]} expansion.

Extended test-vale-wrap.sh's existing bash-3.2 hazard sweep to scan this
file too, and added a check for `declare -A` itself — it previously only
caught unguarded ${arr[@]} expansions and mapfile/readarray, so this
exact regression had no test that would have caught it.

Refs: #85
This commit is contained in:
2026-08-10 07:37:55 +00:00
parent e62f68a1cc
commit 389a4f0f7a
2 changed files with 35 additions and 17 deletions

View File

@@ -447,7 +447,10 @@ fi
# scripts, and the test runner AGENTS.md tells contributors to run by hand.
# `mapfile` is checked alongside, because it is bash 4.0+ and the expansion scan
# cannot see it — run-tests.sh carried one until it was replaced with a
# `while read` loop, and nothing would have caught its return.
# `while read` loop, and nothing would have caught its return. `declare -A`
# (bash 4.0+ associative arrays) is checked for the same reason — the
# expansion scan cannot see it, and check-vale-style-sync.sh carried a pair of
# them until they were replaced with index-scanned plain arrays.
echo ""
echo "--- no unguarded array expansion remains in the macOS-facing scripts ---"
unguarded_expansions() {
@@ -474,6 +477,7 @@ for BASH32_SCRIPT in \
"$SCRIPT" \
"$REPO_ROOT/scripts/skill-size-check.sh" \
"$REPO_ROOT/scripts/check-release-needed.sh" \
"$REPO_ROOT/scripts/check-vale-style-sync.sh" \
"$REPO_ROOT/tests/run-tests.sh"; do
FOUND16="$(unguarded_expansions "$BASH32_SCRIPT")"
if [[ -n "$FOUND16" ]]; then
@@ -486,6 +490,15 @@ for BASH32_SCRIPT in \
if [[ -n "$FOUND16B" ]]; then
HAZARDS16+="${BASH32_SCRIPT##*/}:$FOUND16B "
fi
# `declare -A` (associative arrays) is bash 4.0+ with no 3.2 fallback. The
# flag cluster can carry other letters in any order (-Ag, -rA, ...); what
# matters is a literal uppercase A appearing in it, so match on that rather
# than the exact string "-A".
FOUND16C="$(awk '{ if ($0 ~ /^[[:space:]]*#/) print ""; else print }' "$BASH32_SCRIPT" \
| grep -nE '(^|[^[:alnum:]_])declare[[:space:]]+-[a-zA-Z]*A[a-zA-Z]*([[:space:]]|$)' || true)"
if [[ -n "$FOUND16C" ]]; then
HAZARDS16+="${BASH32_SCRIPT##*/}:$FOUND16C "
fi
done
if [[ -n "$HAZARDS16" ]]; then
fail "unguarded array expansion(s) abort on bash < 4.4 under set -u: $(echo "$HAZARDS16" | tr '\n' ' ')"