Skip to content

fix(bin): make heredoc assignments immune to bash-3.2 quote tracking#269

Open
idrishassan-ai wants to merge 4 commits into
kunchenguid:mainfrom
idrishassan-ai:fm/fix-brief-bash32-4t
Open

fix(bin): make heredoc assignments immune to bash-3.2 quote tracking#269
idrishassan-ai wants to merge 4 commits into
kunchenguid:mainfrom
idrishassan-ai:fm/fix-brief-bash32-4t

Conversation

@idrishassan-ai

Copy link
Copy Markdown

Problem

macOS /bin/bash is 3.2.57, whose parser tracks quote state through a heredoc body nested inside $( ) command substitution.
A lone apostrophe in such a body breaks parsing of the entire script (issue #166):

unexpected EOF while looking for matching `''
syntax error: unexpected end of file

Modern bash (4+) parses the construct fine, so Linux CI never sees the failure.
PR #173 made bash -n pass by rewording the apostrophe away, but left the fragile VAR=$(cat <<EOF ...) construct in place: any future apostrophe in those bodies re-breaks every ship brief on macOS with no CI tripwire.

Fix

  • bin/fm-brief.sh: assign the three ship-mode Definition-of-done heredocs with read -r -d '' DOD <<EOF ... EOF || true instead of $(cat <<EOF ...). bash 3.2 parses this regardless of body quoting. No brief wording changed.
  • bin/fm-bootstrap.sh: convert the shim_body and cadence_body heredocs the same way (cadence_body keeps its quoted no-expansion delimiter). Extends the same hardening so the whole bin/ surface is free of the construct.
  • tests/fm-brief.test.sh: bash -n now also runs explicitly under /bin/bash (the affected 3.2 interpreter on macOS), and a structural test refuses any non-comment $(cat << across bin/*.sh and bin/backends/*.sh, so the trap cannot silently return on Linux CI where bash -n cannot see it.
  • docs: the bash-3.2 heredoc convention is recorded alongside the updated test guard.

Evidence (verified on /bin/bash 3.2.57, arm64-apple-darwin25)

  • With an apostrophe injected into a DOD body, the old construct fails -n (exit 2, errors above); the new construct passes.
  • All five generated briefs are byte-identical before vs after, built from the same FM_HOME: ship no-mistakes, ship direct-PR, ship local-only, --scout, --secondmate. The ship no-mistakes brief hashes to the same sha256 (142a8e66…) on both sides.
  • Bootstrap-generated shim/cadence artifact contents are unchanged.
  • Every script in bin/, bin/backends/, and tests/ passes /bin/bash -n under 3.2; tests/fm-brief.test.sh passes under both bash (5.x) and /bin/bash (3.2).
  • Validated by the no-mistakes pipeline: review, test, document, and lint all green with no findings (push required this fork PR because the branch author has no upstream push access).

🤖 Generated with Claude Code

The three ship-mode Definition-of-done blocks in bin/fm-brief.sh were
assigned via DOD=$(cat <<EOF ... EOF). bash 3.2 (macOS /bin/bash 3.2.57)
tracks quote state through a heredoc body nested inside $( ) while
scanning for the closing paren, so a lone apostrophe anywhere in that
body breaks parsing of the entire script:

    $ /bin/bash -n <old construct with "no-mistakes' own guidance">
    unexpected EOF while looking for matching `''
    syntax error: unexpected end of file
    exit=2

PR kunchenguid#173 fixed the immediate failure by rewording the apostrophe away,
but left the fragile construct in place: any future apostrophe in a DOD
body re-breaks every ship brief on macOS, and Linux CI (bash 4+) parses
the construct fine, so `bash -n` there can never catch a regression.

Fix per the brief-wording contract (wording is deliberate; prefer a
structural fix over rewording): assign each DOD with

    read -r -d '' DOD <<EOF || true

which bash 3.2 parses regardless of body quoting and which yields the
same value as $(cat ...) for these bodies (default IFS strips the
trailing newline exactly as command substitution does). No brief text
changed.

Evidence, verified on /bin/bash 3.2.57 (arm64-apple-darwin25):
- /bin/bash -n bin/fm-brief.sh passes before and after (already green
  at HEAD since kunchenguid#173); with an apostrophe injected into a DOD body, the
  old construct fails -n (exit 2, errors above) and the new one passes.
- All five generated briefs are byte-identical before vs after, built
  from the same FM_HOME with the HEAD script and the fixed script:
  ship no-mistakes, ship direct-PR, ship local-only, --scout, and
  --secondmate. diff is empty for each; ship no-mistakes brief sha256
  142a8e6620c748fd01c1053cde9cf98b6edb209480336ec8bc5e7c6509a74c68 on
  both sides.
- tests/fm-brief.test.sh passes under both `bash` and /bin/bash 3.2.
- Audit of bin/*.sh, bin/backends/*.sh, tests/*.sh: every script passes
  /bin/bash -n under 3.2. The remaining $(cat <<EOF) instances
  (bin/fm-bootstrap.sh:298,309 and quoted-delimiter fixtures in
  tests/fm-crew-state.test.sh) contain no single quotes and parse
  cleanly, so they are left untouched.

tests/fm-brief.test.sh grows two guards: `bash -n` now also runs under
/bin/bash explicitly (the affected interpreter on macOS), and a
structural test refuses any non-comment $(cat <<...) in fm-brief.sh so
the trap cannot return silently on Linux CI where -n cannot see it.
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