fix(portability): replace bash 4 builtins absent from macOS bash 3.2

macOS ships bash 3.2 as /bin/bash, which `#!/usr/bin/env bash` resolves to
when no newer bash is on PATH. Two builtins the repo relies on do not exist
there, and both failed SILENTLY:

- `mapfile` in the three surgical-commit helpers left every array empty, so
  the scope guards passed on nothing (fail-OPEN) and the commits degraded to
  "nothing pending — no-op" while reporting success. deploy-commit.test.sh
  went 4/16; memory and doc commits simply never happened.
- `declare -A` in the session-start hook errored on every session and left
  each plugin cost at 0, so the passive-budget warning could never fire.

`_read_lines_into` is the portable equivalent of `mapfile`, space-safe and
resetting its target first — expanding a never-assigned array trips `set -u`
on bash < 4.4, which is how the empty arrays surfaced as "unbound variable".
Plugin costs move to a `case`.

source-scope.sh's header prescribed `mapfile` to its callers; it now shows
the read loop, and its own test plus run-reconcile.sh stop using the builtin.

deploy-commit 16/16, source-scope 34/34, run-reconcile 25 GREEN / 0 RED,
session-start stderr empty.
This commit is contained in:
2026-09-13 17:20:57 -04:00
parent a53a5a26a8
commit f3919b6ace
7 changed files with 82 additions and 24 deletions
+5 -2
View File
@@ -4,7 +4,9 @@
# EXCL="$(bash ~/.claude/lib/source-scope.sh grep)"
# grep -rl "gtag" $EXCL --include="*.html" . # note: $EXCL unquoted
#
# mapfile -t FEXCL < <(bash ~/.claude/lib/source-scope.sh findargs)
# FEXCL=(); while IFS= read -r t; do FEXCL+=("$t"); done \
# < <(bash ~/.claude/lib/source-scope.sh findargs)
# (a read loop, not mapfile: macOS /bin/bash is 3.2 and has no mapfile)
# find . "${FEXCL[@]}" -iname '*.jpg' -printf '%s %p\n' # quoted array!
#
# findargs emits ONE TOKEN PER LINE and MUST be consumed through a quoted
@@ -72,7 +74,8 @@ case "${1:-}" in
list) _list ;;
# Safe unquoted: --exclude-dir=NAME carries no glob character.
grep) _list | while read -r d; do printf -- '--exclude-dir=%s ' "$d"; done; echo ;;
# One token per line — consume with mapfile + a QUOTED array, never a flat
# One token per line — consume with a read loop into a QUOTED array (see
# the header: mapfile is bash 4+), never a flat
# string (see header: the shell would glob */dist/* against the CWD).
findargs) _list | while read -r d; do printf '!\n-path\n*/%s/*\n' "$d"; done ;;
*) _die "usage: source-scope.sh {list|grep|findargs}" ;;