Skip to content

fix(ci): shell-portability-lint's sed -Ei / --in-place tokens miss operator-terminated forms - #1548

Merged
kyle-sexton merged 1 commit into
mainfrom
fix/1545-sed-ei-inplace-operator-terminated-boundary
Jul 26, 2026
Merged

fix(ci): shell-portability-lint's sed -Ei / --in-place tokens miss operator-terminated forms#1548
kyle-sexton merged 1 commit into
mainfrom
fix/1545-sed-ei-inplace-operator-terminated-boundary

Conversation

@kyle-sexton

Copy link
Copy Markdown
Contributor

This was generated by AI during work-loop execution.

Closes #1545

Summary

Test plan

  • bash scripts/check-shell-portability.test.sh — 75/75 passing (6 new regression cases: 3
    operator-terminated forms each for sed -Ei and sed --in-place at the isolated-token
    level, plus one shipped-list assertion proving all 4 forms are detected under the real
    shell-portability-tokens.txt).
  • Verified the new tests are meaningful: reproduced the bug first (bash scripts/check-shell-portability.sh --paths t.sh against a file containing the four
    operator-terminated forms passed clean on main's shipped list); after the fix, all four
    are flagged.
  • Verified the boundary is not over-widened: the full sibling (quote-inclusive) boundary was
    tried first and demonstrably broke the pre-existing sed -Ei'' deferred-ambiguous-suffix
    test (PASS=74 FAIL=1); narrowed to exclude quotes/backtick, re-ran — PASS=75 FAIL=0.
  • scripts/check-shell-portability.sh --all — before/after hit sets identical (75 findings,
    unrelated to these tokens); see summary above.
  • scripts/check-shell-portability.sh origin/main run directly against this branch's own
    diff — clean (no unexcused GNU-only constructs in the 2 changed files).
  • shellcheck --rcfile=.shellcheckrc scripts/check-shell-portability.test.sh — clean.
  • typos --config _typos.toml on both changed files — clean.

Related

…erator-terminated forms

Closes #1545

The two GNU-only sed tokens added by #1513/#1534 (-Ei combined-cluster and
--in-place long-form) only accepted a trailing whitespace-or-end-of-line
boundary, so an operator-terminated form with no separating whitespace
(x=$(sed -Ei), sed -Ei|cat, sed --in-place|cat, x=$(sed --in-place)) evaded
detection entirely -- the same class of false negative #1537 named for
sort -V / grep -P / echo -e.

Widens both tokens' trailing boundary to a control operator, redirection, or
subshell close ([[:space:]|&;()<>]), NOT a quote or backtick like the
sibling tokens #1537/#1546 widen to: a quote immediately following -Ei with
no separating whitespace is exactly the attached-EMPTY-suffix shape (-Ei'')
#1513/#1534 deliberately deferred as ambiguous sed-dialect territory, so
including quotes in the boundary would have silently un-deferred it.
Confirmed via the isolated-token regression suite: the full sibling boundary
(quotes included) breaks the existing "must not flag -Ei''" test.

Verified before landing: check-shell-portability.sh --all hit sets are
byte-for-byte identical before and after (75 pre-existing findings, none
sed-related), so the widened boundary surfaces no new corpus violations.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@claude

claude Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 2m 57s —— View job


PR Review — fix(ci): sed -Ei / --in-place operator-terminated boundary

  • Gather context and read changed files
  • Review scripts/shell-portability-tokens.txt changes
  • Review scripts/check-shell-portability.test.sh changes
  • Verify regex correctness and boundary logic
  • Check test coverage and regression cases
  • Post final review

Summary

The fix is correct and well-reasoned. Two minor issues found; one very minor nit on a test comment.


Regex analysis

Both new boundary expressions use ([[:space:]|&;()<>]|$). This is valid POSIX ERE:

  • Inside a bracket expression [...], | is a literal pipe character, not the alternation operator — so [[:space:]|&;()<>] matches one character from: whitespace, |, &, ;, (, ), <, >.
  • The outer (…|$) alternates between "one boundary character" and "end of line."

This is the right set. Quotes and backtick are correctly absent, preserving the sed -Ei'' deferral the comment explains. The narrower boundary versus the sibling sort -V/grep -P/echo -e tokens is exactly right given the ambiguous empty-suffix constraint.


Findings

1. Misleading comment in test file — "or quote must still be detected"

check-shell-portability.test.sh lines 402–403:

# --- operator-terminated forms: no trailing whitespace before a control
# operator, redirection, subshell close or quote must still be detected

The phrase "or quote must still be detected" implies quote-terminated forms are included in the detection, but the whole point of this PR's narrower boundary is that they are notsed -Ei'' stays deliberately undetected. The comment should not enumerate quotes among the terminators covered. A reader seeing this comment without the rest of the context would reasonably conclude sed -Ei'...' is flagged.

Suggested rewrite:

# --- operator-terminated forms: no trailing whitespace before a control
# operator, redirection, or subshell close must still be detected

Fix this →

2. Shipped-list test omits ;-terminated forms (minor coverage gap)

check-shell-portability.test.sh lines 754–758:

f="$(tmpsh "$(printf '%s\n' \
  'x=$(sed -Ei)' \
  'sed -Ei|cat' \
  'x=$(sed --in-place)' \
  'sed --in-place|cat')")"

The PR description names four forms as the bug's concrete scope, and the shipped-list check proves those four against the real token file. However, sed -Ei; echo done and sed --in-place; echo done — which the isolated-token loops do cover — are absent here. ; is the most common command separator and is also in the boundary. Since the isolated-token tests already exercise ;, this is not a correctness gap, but if the shipped-list test is meant to be the definitive "real-token-file proof," including the ; forms would make it exhaustive.

Not blocking — the isolated-token path already covers ; — but worth noting.


What's correct

  • The two isolated-token while-loops (CASES heredocs) correctly check detection by expecting scan_paths to exit non-zero (violation found), consistent with every other detection test in the file.
  • Cleanup is correct: rm -f "$f" inside the loop, rm -f "$tok" after the loop.
  • Using case as the read variable name mirrors the pre-existing pattern at line 235 (already in origin/main), not a new smell.
  • The grep -c "PORTABILITY: ${f}:" assertion in the shipped-list test is the established counting pattern (matches the sort long-form test above it).
  • Token-file comment is thorough and accurate: explicitly documents the quote-exclusion rationale, the #1545 reference, and contrasts the boundary choice with the sibling tokens.

Findings: 1 comment accuracy issue (non-blocking), 1 minor coverage gap in the shipped-list test. Core fix is sound — approve when the comment wording is addressed or acknowledged.

@claude

claude Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 2m 1s —— View job


Security Review — PR #1548

  • Gather context and read changed files
  • Analyze for security vulnerabilities
  • Post findings

No security issues found.

This PR makes two narrow regex-widening changes to a developer linting tool (shell-portability-tokens.txt) and adds corresponding test cases. Here is what I checked:

Pattern consumption path — The ERE patterns in shell-portability-tokens.txt are consumed at check-shell-portability.sh:224 via awk's ~ operator (line ~ patterns[i]). This is awk's built-in regex matching, not a system() call, eval, or shell interpolation, so there is no code-execution surface here. A malformed or malicious pattern causes awk to exit non-zero, which the script already handles by failing closed (line 240–244).

ReDoS — The ([[:space:]][^\n]*)?[[:space:]] segment, which could in theory cause catastrophic backtracking, is not new to this PR — it is carried forward unchanged from the tokens these patterns widened. awk implementations (GNU awk uses a DFA/NFA hybrid) handle this class efficiently in practice, and the files being scanned are developer-controlled scripts in CI, not adversarial user input.

Bracket-expression correctness[[:space:]|&;()<>] is syntactically valid: inside a POSIX bracket expression, | is a literal character, not ERE alternation. The intent (match pipe, ampersand, semicolon, parentheses, angle brackets, or whitespace) is correctly expressed. The |$ trailing alternation for end-of-line is correctly placed outside the brackets.

Test script injection — The new while IFS= read -r case; do loops in check-shell-portability.test.sh write test content to temp files via printf '%s\n' "$1" inside tmpsh() and then pass the file path to the checker — the content is scanned as text, never executed as shell. No injection risk.

No workflow files modified — No .github/workflows/ changes; no CI/CD injection surface (untrusted code with elevated permissions, pull_request_target, script injection through the github context, or permission widening).

No secrets, credentials, or supply-chain changes — None present.

@kyle-sexton
kyle-sexton merged commit 6448968 into main Jul 26, 2026
30 checks passed
@kyle-sexton
kyle-sexton deleted the fix/1545-sed-ei-inplace-operator-terminated-boundary branch July 26, 2026 12:51
kyle-sexton added a commit that referenced this pull request Jul 26, 2026
…ed-list coverage (#1549)

*This was generated by AI during work-loop execution.*

No linked issue

## Summary

- #1548 (`sed -Ei` / `--in-place` operator-terminated boundary fix,
closing #1545) picked up two
non-blocking findings from its automated post-green review before it was
merged out from under
this follow-up by a concurrent auto-merge (worker-tier `babysit-prs`,
gate-proven `c2-mechanical`)
— the fix commit landed but a second, already-written commit addressing
the review findings
never made it in. This PR carries that second commit forward against
current `main`.
- **Comment accuracy.** The operator-terminated regression-case comment
said "...or quote must
still be detected", which is backwards for this specific token pair:
quotes are deliberately
*excluded* from the `sed -Ei`/`--in-place` boundary (unlike the sibling
`sort -V`/`grep -P`/
`echo -e` tokens) — that exclusion is the entire point of #1548's
narrower boundary, preserving
the `sed -Ei''` attached-empty-suffix deferral from #1513/#1534.
Reworded and pointed at the
`"sed -Ei'' must not be flagged"` test that explains why. Also corrected
a stale claim in the
shipped-list assertion's own comment, which likewise implied these two
tokens share the sibling
  tokens' (quote-inclusive) boundary.
- **Shipped-list test completeness.** The shipped-list assertion proved
4 of the 6
operator-terminated forms the isolated-token loops cover (missing the
`;`-terminated pair for
both tokens). Not a correctness gap — the isolated-token path already
exercises `;` — but the
shipped-list check is meant to be the exhaustive real-token-file proof,
so added the two missing
  cases.

## Test plan

- [x] `bash scripts/check-shell-portability.test.sh` — 75/75 passing
(unchanged pass count; this
PR only edits comments and extends one assertion's input set from 4 to 6
lines).
- [x] `scripts/check-shell-portability.sh origin/main` run directly
against this branch's own
diff — clean (no unexcused GNU-only constructs in the 1 changed file).
- [x] `shellcheck --rcfile=.shellcheckrc
scripts/check-shell-portability.test.sh` — clean.
- [x] `typos --config _typos.toml
scripts/check-shell-portability.test.sh` — clean.

## Related

- #1548 (the PR these findings were raised against, and whose second
commit this one carries
  forward) — review comment:

#1548 (comment)
- #1545 (the issue #1548 closed)

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
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.

fix(ci): shell-portability-lint's sed -Ei / --in-place tokens miss operator-terminated forms

1 participant