From 62c676e95be83de28676563679a043ca4090fc43 Mon Sep 17 00:00:00 2001 From: Jonathan Borduas Date: Tue, 8 Sep 2026 05:21:12 +0100 Subject: [PATCH 1/2] =?UTF-8?q?pipe-exit-scan:=20catch=20the=20SUBSTITUTIO?= =?UTF-8?q?N=20clobber=20=E2=80=94=20and=20it=20fires=20on=2027=20real=20c?= =?UTF-8?q?ommands,=203=20of=20them=20mine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #375. Its population was defined by the SYNTAX it was first seen in rather than by the QUESTION it answers — #307's glob-that-did-not-recurse, in a regex. ⛔ THE SPECIMEN, DEVOPS's, verbatim in the fixture rather than reconstructed: python3 "$s" --self-test --zzz-not-a-flag >/dev/null 2>&1 printf " %-34s rc=%s\n" "$(basename $s)" "$?" `$(basename $s)` is evaluated FIRST and resets `$?`. Six subjects read as accepting a bogus flag; the true codes were 2,2,2,2,0,0 — and two of those zeros were real defects. ★ NOTHING IS PIPED, so `pipeline_status_read` structurally cannot see it. THE VERDICT COMES FROM ORDER, not from the presence of two tokens — same shape as the pipe predicate it sits beside. ⚠ And the span must CLOSE before the read: x=$(foo $?) the `$?` is INSIDE the span — it is the PREVIOUS command's status and is CORRECT. Flagging it fires on an agent doing the right thing, which this file already calls the worst kind of guard. f "$(g)" "$?" the span closes first — the `$?` is g's. The defect. ⇒ IT FIRES ON REAL COMMANDS. Tracked files: 0 — the tree is clean of this. TRANSCRIPTS: 27 findings across executed commands, and the first three are session 15b69750's own: PYTHONDONTWRITEBYTECODE=1 python3 "$t" >/dev/null 2>&1; \ printf " %-34s exit=%s\n" "$(basename $t)" "$?" ⇒ I committed the defect I am fixing, at least three times, in the reporter's exact shape. ⚠ My own session is in the corpus (#296 §5); these three are executed probes from earlier cycles, not this discussion of them. CONDITION, all three legs of #375: 1 ✅ reports `$?` read after a command substitution 2 ✅ KNOWN-NEGATIVES, four, none firing: `x=$(foo $?)` · `RC=$?` captured before any substitution (the report's own stated negative) · a `$RC` printf · `$((1+2)) $?` 3 ✅ the fixture carries the MEASURED specimen, not a reconstruction ⛔ the leg DISCRIMINATES: replacing the span test with `if True` gives 5 findings where 2 are expected, and the self-test exits 2. Without the negatives a predicate that fired on every `$?` in the repo would pass this leg. Wired into BOTH call sites — tracked files and transcripts — because a predicate with no caller is the defect this board has found four times today. Co-Authored-By: Claude Opus 5 (1M context) --- tools/pipe-exit-scan.py | 96 +++++++++++++++++++++++++++ tools/testdata/subst-exit-positive.sh | 23 +++++++ 2 files changed, 119 insertions(+) create mode 100644 tools/testdata/subst-exit-positive.sh diff --git a/tools/pipe-exit-scan.py b/tools/pipe-exit-scan.py index e7f1910..03cdcbd 100755 --- a/tools/pipe-exit-scan.py +++ b/tools/pipe-exit-scan.py @@ -104,6 +104,72 @@ def pipeline_status_read(line): return False # `${PIPESTATUS[n]}` — bash-only. Empty in zsh, and empty is not zero. +# ── The SUBSTITUTION clobber — same consequence, different mechanism (#375) ── +# +# ⛔ THE SPECIMEN, measured by DEVOPS while citing this very instrument an hour earlier: +# +# python3 "$s" --self-test --zzz-not-a-flag >/dev/null 2>&1 +# printf " %-34s rc=%s\n" "$(basename $s)" "$?" +# +# `$(basename $s)` is evaluated FIRST and resets `$?`. Every code printed was +# `basename`'s 0, not the subject's — the true values were 2, 2, 2, 2, 0, 0, and two +# of those zeros were real defects. +# +# ★ NOTHING IS PIPED HERE, so `pipeline_status_read` above cannot see it. Same class, +# same consequence — a confident reading of the WRONG process's exit code — and this +# file's population was defined by the SYNTAX it was first seen in rather than by the +# QUESTION it answers. That is #307's glob-that-did-not-recurse, in a regex. +# +# ⚠ THE SPAN MUST BE CLOSED BEFORE THE READ, and that is not a nicety: +# x=$(foo $?) the `$?` is INSIDE the substitution — it is the PREVIOUS +# command's status and is correct. Flagging it would fire on an +# agent doing the right thing, which this file calls the worst +# kind of guard. +# f "$(g)" "$?" the span CLOSES first — the `$?` is g's. The defect. +SUBST_OPEN = re.compile(r"\$\((?!\()|`") + + +def _closed_substitution_ends(seg): + """End offsets of every substitution that CLOSES within this segment.""" + ends, i, n = [], 0, len(seg) + while i < n: + if seg.startswith("$(", i) and not seg.startswith("$((", i): + depth, j = 1, i + 2 + while j < n and depth: + if seg[j] == "(": + depth += 1 + elif seg[j] == ")": + depth -= 1 + j += 1 + if depth == 0: + ends.append(j) + i = j + elif seg[i] == "`": + j = seg.find("`", i + 1) + if j == -1: + break + ends.append(j + 1) + i = j + 1 + else: + i += 1 + return ends + + +def substitution_status_read(line): + """Does `$?` here read the status of a COMMAND SUBSTITUTION that ran first? + + Same segment split as `pipeline_status_read`, and the same shape of answer: the + verdict comes from ORDER, not from the presence of the two tokens.""" + segs = re.split(r"(? before: per[base] = len(hits) - before return hits, per @@ -361,6 +431,9 @@ def scan_shell(path): hits.append((n, raw.strip(), "$? read after a pipeline — that is the LAST element's status")) elif PIPESTATUS.search(code): hits.append((n, raw.strip(), "PIPESTATUS — bash-only; expands EMPTY in zsh, and empty is not zero")) + elif substitution_status_read(code): + hits.append((n, raw.strip(), "$? read after a COMMAND SUBSTITUTION — the substitution " + "ran first and reset it (#375)")) # ⛔ Second matcher, second defect. Kept as its own pass because it needs the # WHOLE file (function bodies, and what is read after the loop), which the # line-at-a-time loop above structurally cannot see. @@ -397,6 +470,8 @@ def scan_markdown(path): # ⛔ Its own fixture, holding POSITIVES AND NEGATIVES together: a fixture of only # positives cannot distinguish "detects the defect" from "fires on every while loop". SELFTEST_SUBSHELL = "tools/testdata/subshell-positive.sh" +# ⛔ #375: the MEASURED specimen, not a reconstruction — the report requires that. +SELFTEST_SUBST = "tools/testdata/subst-exit-positive.sh" SELFTEST_NEGATIVE = "tools/README.md" @@ -430,6 +505,27 @@ def selftest(): else: print(f" FAIL known-negative: fired on prose about the trap — {neg}") ok = False + # ── the SUBSTITUTION clobber, both directions in one fixture (#375) ────── + # + # ⛔ The fixture's first block is DEVOPS's measured specimen verbatim. Its + # known-negatives are the two that decide whether this predicate is usable at + # all: `x=$(foo $?)` reads the PREVIOUS command's status and is CORRECT, and a + # `$?` captured to a variable before any substitution is the shape #375 names. + # ⚠ Without them a predicate that fired on every `$?` in the repo would pass + # this leg and report a count carrying no information. + subst = scan_shell(SELFTEST_SUBST) + subst_real = [h for h in subst if "SUBSTITUTION" in h[2]] + if len(subst_real) == 2: + print(f" ok substitution known-positive: 2 findings in {SELFTEST_SUBST}") + for n_, s_, _ in subst_real: + print(f" L{n_}: {s_[:72]}") + print(" ok substitution known-negative: 0 of 4 fired — `x=$(foo $?)` (the read is " + "INSIDE the span), `RC=$?`, a `$RC` printf, and `$((1+2)) $?`") + else: + print(f" FAIL substitution: {len(subst_real)} findings, expected exactly 2 — " + f"either the specimen does not fire or a known-negative does") + ok = False + # ── the subshell matcher, both directions in one fixture ───────────────── sub = scan_shell_subshell(SELFTEST_SUBSHELL) if len(sub) == 3: diff --git a/tools/testdata/subst-exit-positive.sh b/tools/testdata/subst-exit-positive.sh new file mode 100644 index 0000000..b580d8d --- /dev/null +++ b/tools/testdata/subst-exit-positive.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# ⛔ FIXTURE for #375. The first block is the MEASURED SPECIMEN, verbatim from the +# report — not a reconstruction. DEVOPS ran it while citing pipe-exit-scan an hour +# earlier, and read six subjects as accepting a bogus flag; the true codes were +# 2, 2, 2, 2, 0, 0, and two of those zeros were real defects. + +for s in tools/*.py; do + python3 "$s" --self-test --zzz-not-a-flag >/dev/null 2>&1 + printf " %-34s rc=%s\n" "$(basename $s)" "$?" +done + +# a second shape of the same defect: the span closes, then the read +f "$(g)" "$?" + +# ── KNOWN-NEGATIVES. None of these may fire. ── +# the `$?` is INSIDE the substitution: it is the PREVIOUS command's status, correct. +x=$(foo $?) +# captured to a variable BEFORE any substitution — #375's stated known-negative. +python3 tool.py >/dev/null 2>&1 +RC=$? +printf " %-34s rc=%s\n" "$(basename x)" "$RC" +# arithmetic expansion is not a command substitution. +echo $((1+2)) $? From 592b333c112cc6a013e5cf8296be97370fe45401 Mon Sep 17 00:00:00 2001 From: Jonathan Borduas Date: Tue, 8 Sep 2026 05:40:06 +0100 Subject: [PATCH 2/2] =?UTF-8?q?pipe-exit-scan:=20judge=20every=20`$=3F`=20?= =?UTF-8?q?against=20the=20span=20containing=20it=20=E2=80=94=20review=20f?= =?UTF-8?q?ound=20both=20halves?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two review findings adopted, one declined with the bash output that decides it. 1. ⛔ THE SHORTCUT HAD BOTH FAILURE DIRECTIONS, and the reviewer named both. The test was "did any substitution close before the FIRST `$?`", which is wrong twice: printf '%s\n' "$(true)" "$(printf '%s' "$?")" the `$?` is INSIDE the second span — it reads the first substitution's status, which is what that code MEANS. Reported as a defect. FALSE POSITIVE. echo "$(a $?)" "$?" the first `$?` sits inside a span, the loop returned on it, and the real read outside was never reached. MISSED. ⇒ Spans are now RANGES and every `$?` is classified independently: inside a span it is the previous status and correct; outside one that closed first, it is the defect. Both cases are in the fixture now, as a positive and a negative. 2. ⛔ THE LEG ASSERTED A COUNT AND NOW ASSERTS THE LINES. `len(subst_real) == 2` passes when a regression misses one required positive AND fires on one known-negative — two errors that cancel. ⇒ `{9, 13, 29}`, and the failure message names which line moved. ★ That is #636's lesson applied to a suite: a count cannot be re-verified, a set can. Demonstrated — breaking the span-containment test now reports `fired on lines [9, 13, 29, 32], expected exactly [9, 13, 29]`, naming line 32. DECLINED — "track completed substitutions across top-level command boundaries", with the bash output that settles it: value=$(false); printf '%s' "$?" -> 1 the ASSIGNMENT's own status. CORRECT. true; x=$(false); printf '%s' "$?" -> 1 true's status is gone. THE DEFECT. ⇒ The class is real, and it is NOT MECHANICALLY SEPARABLE from correct code: both are `; `, and only intent distinguishes them. Flagging it fires on `value=$(false); echo $?`, which is right — the worst kind of guard by this file's own stance, six lines above. Named as a bound rather than implemented. Co-Authored-By: Claude Opus 5 (1M context) --- tools/pipe-exit-scan.py | 57 ++++++++++++++++++--------- tools/testdata/subst-exit-positive.sh | 9 +++++ 2 files changed, 48 insertions(+), 18 deletions(-) diff --git a/tools/pipe-exit-scan.py b/tools/pipe-exit-scan.py index 03cdcbd..189879c 100755 --- a/tools/pipe-exit-scan.py +++ b/tools/pipe-exit-scan.py @@ -129,9 +129,9 @@ def pipeline_status_read(line): SUBST_OPEN = re.compile(r"\$\((?!\()|`") -def _closed_substitution_ends(seg): - """End offsets of every substitution that CLOSES within this segment.""" - ends, i, n = [], 0, len(seg) +def _substitution_spans(seg): + """(start, end) for every substitution that CLOSES within this segment.""" + spans, i, n = [], 0, len(seg) while i < n: if seg.startswith("$(", i) and not seg.startswith("$((", i): depth, j = 1, i + 2 @@ -142,31 +142,45 @@ def _closed_substitution_ends(seg): depth -= 1 j += 1 if depth == 0: - ends.append(j) + spans.append((i, j)) i = j elif seg[i] == "`": j = seg.find("`", i + 1) if j == -1: break - ends.append(j + 1) + spans.append((i, j + 1)) i = j + 1 else: i += 1 - return ends + return spans def substitution_status_read(line): """Does `$?` here read the status of a COMMAND SUBSTITUTION that ran first? - Same segment split as `pipeline_status_read`, and the same shape of answer: the - verdict comes from ORDER, not from the presence of the two tokens.""" + ⛔ EVERY `$?` IS JUDGED AGAINST THE SPAN THAT CONTAINS IT, not against "was there + an earlier one". Review of #375's first version found both halves of that shortcut: + + printf '%s\n' "$(true)" "$(printf '%s' "$?")" + the `$?` is INSIDE the second span — it reads the first substitution's + status, which is what that code MEANS. The old test saw a span closed + before it and reported a defect. FALSE POSITIVE. + + "$(a)$?" where an earlier `$?` sits inside a span + the old test returned on the first match and never reached the real one. + MISSED. + + ⇒ Spans are ranges; each read is classified independently.""" segs = re.split(r"(?