Conversation
|
@claude is attempting to deploy a commit to the Vercel Labs Team on Vercel. A member of the Team first needs to authorize it. |
🤖 auto-maintain reviewAutomated, advisory triage for
Review panel: 🟡 medium highest severity just-bash maintainer code review: 🟡 medium
General code review: 🟢 low
Adversarial security: 🟢 low
Adversarial security (second opinion): 🟢 low
Standard Bash and host portability: 🟢 low
Posted by auto-maintain. This automated code review is advisory; a human maintainer makes the call. |
…ult}
Under `set -u`, a word that is exactly one double-quoted part holding
exactly one `${var<op>word}` is routed through `handleArrayDefaultValue()`,
which read the variable with nounset still armed. Every operator that is
supposed to suppress nounset was affected: `:-`, `-`, `:=`, `=`, `:+`, `+`.
Adjacent literal text ("${U:-d}b", "a${U:-d}") takes the general path,
which already computes `skipNounset`, so the failure looked arbitrary.
`"${#var}"` and a bare `"${var}"` still report an unbound variable, as
in GNU bash.
Reported downstream as ai-ecoverse/slicc#2978.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Lars Trieloff <lars@trieloff.net>
a1c75d8 to
61e4f3f
Compare
|
Thanks — the finding is real, but the attribution is wrong: it is a pre-existing defect, not something this PR introduces. The claim is that "disabling nounset now reaches the array-default shortcut". The shortcut is reached whenever the outer variable is unset, which does not require $ printf 'arr=(x y)\nprintf "<%%s>" "${U:-pre${arr[@]}post}"; echo\n' | pnpm dev:exec --real-bash
just-bash: <x><y>
real bash: <prex><ypost>So the behaviour is identical before and after this change. What the change does is make that branch reachable under The actual defect is narrower than "drops adjacent operation-word parts": it only bites when the default word mixes an array expansion with adjacent literal text.
( The correct semantics are the prefix/suffix gluing that I have deliberately left that out of this PR, which is a one-line nounset fix: it is an orthogonal pre-existing bug, the correct fix is not a one-liner, and it deserves its own comparison-test coverage. Filed separately as #419. |
The comparison-tests CI job re-records fixtures on Linux and fails on any diff. The two "must still error" fixtures were recorded against macOS bash 3.2, whose unbound-variable diagnostic omits the "line 1: " prefix that bash 5 emits, so re-recording drifted them. Adjust both to the Linux form and mark them locked, as CLAUDE.md prescribes for fixtures adjusted to Linux behaviour. Record mode is now idempotent. The recorded stderr is never compared -- compareOutputs only checks stdout and (here, disabled) exit code -- so this only stops drift. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The bug
Under
set -u, a word that is exactly one double-quoted part containing exactly one${var<op>word}reports "unbound variable", even though the operator is supposed to suppress nounset. Adjacent literal text takes a different code path and already worked, which made the failure look arbitrary:Every operator that suppresses nounset was affected:
:-,-,:=,=,:+,+.The practical impact is that the idiomatic "works inside and outside GitHub Actions" guard could not run at all:
Root cause
That exact AST shape (
wordParts.length === 1&&DoubleQuoted&&dqPart.parts.length === 1&&DefaultValue/UseAlternative/AssignDefault) is claimed byhandleArrayDefaultValue()inpackages/just-bash/src/interpreter/expansion/array-prefix-suffix.ts, reached fromword-glob-expansion.ts:276. Its scalar branch probed the current value withgetVariable(ctx, varName)atarray-prefix-suffix.ts:124, andgetVariable'scheckNounsetparameter defaults totrue(expansion/variable.ts:119-124), so nounset fired before the operator ever got a chance to supply the default.The general path already gets this right —
expansion.ts:1000-1009computesskipNounsetforDefaultValue/AssignDefault/UseAlternative/ErrorIfUnsetand passes!skipNounset. This change makes the scalar branch agree by passingfalse.Scope of the fix
One line plus a comment. Checked and deliberately left alone:
arrayMatch) of the same function reads throughgetArrayElements()/ctx.state.env.get(), neither of which consults nounset — verified"${arr[@]:-d}"and"${arr[@]:+d}"were already correct, and there is a regression test for them.isVariableSet()does not check nounset either, so it needed no change."${#U}"and a bare"${U}"still report an unbound variable underset -u, matching GNU bash. There are explicit tests pinning that.Behaviour changes only for the previously-throwing case: for a set variable,
checkNounsethas no effect on the returned value.Verification
Every operator now matches system bash byte for byte via
pnpm dev:exec --real-bash:Tests
src/comparison-tests/nounset-quoted-default.comparison.test.ts(+ recorded fixtures) — all six operators as whole double-quoted words, the set/empty/unset distinction, the array default, the[ -n "${VAR:-}" ]guard end to end, and the two shapes that must still error.src/interpreter/expansion/nounset-quoted-default.test.ts— the same matrix asserting full stdout and stderr plus exit status, includingbash: JB_UNSET: unbound variable\n/ exit 1 for"${#JB_UNSET}"and"${JB_UNSET}".The two must-still-error comparison cases pass
compareExitCode: false:bash -creports an expansion error as 127 while just-bash reports 1. That is a pre-existing, unrelated divergence I did not touch; the unit tests pin just-bash's own status and message, and the comparison still asserts stdout parity (nothing printed,echo reachednever runs).Reverting just the changed line makes 5 tests in each new file fail, and restoring it makes them pass.
Validation
pnpm typecheck,pnpm lint(incl. banned-patterns + workflow security), andpnpm knipare clean.pnpm test:runis 15632 passed / 6 failed — the 6 failures are pre-existing and environmental (Python WASM 5s timeouts and real-DNS lookups insrc/security/**andsrc/network/allow-list/dns-rebinding-integration.test.ts); they reproduce identically with this change reverted.Credit
Reported downstream as ai-ecoverse/slicc#2978, filed there as "
set -uexpands variables in a non-takenifbranch". That diagnosis was wrong — untaken branches are never evaluated — but the report is what surfaced this, and the guard in it is the real-world script that could not run.🤖 Generated with Claude Code