Conversation
Test Results 5 files 49 suites 23m 59s ⏱️ Results for commit 388a2ca. ♻️ This comment has been updated with latest results. |
Public API changes for crate: brush-coreRemoved itemsAdded itemsChanged itemsPerformance Benchmark ReportCode Coverage Report: Only Changed Files listed
Minimum allowed coverage is Minimum allowed coverage is Test Summary: bash-completion test suite
|
4041c36 to
6a4e2bb
Compare
b8c38bd to
e5387e2
Compare
There was a problem hiding this comment.
Pull request overview
Centralizes assignment expansion in brush-core and improves BASH_COMMAND source reporting.
Changes:
- Adds target-aware assignment expansion APIs.
- Updates interpreter command expansion and tracing.
- Enables previously failing compatibility cases.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
brush-core/src/expansion.rs |
Adds centralized assignment expansion. |
brush-core/src/shell/expansion.rs |
Exposes assignment APIs through Shell. |
brush-core/src/interp.rs |
Uses centralized expansion and source text. |
brush-core/src/commands.rs |
Updates tracing and command arguments. |
brush-shell/tests/cases/compat/options/functrace.yaml |
Enables compatibility cases. |
brush-shell/tests/cases/compat/options/extdebug.yaml |
Enables a BASH_COMMAND case. |
brush-shell/tests/cases/compat/builtins/trap.yaml |
Enables an ERR-trap case. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
e5387e2 to
2c050a7
Compare
2c050a7 to
1e9ae58
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Readonly array elements can be mutated, and inherited associative arrays use incorrect subscript resolution.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 19/19 changed files
- Comments generated: 2
- Review effort level: Balanced
4c46dc1 to
14c13c4
Compare
8243d37 to
3261c47
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Clippy failure, feature coupling, and unversioned public API breaks remain unresolved.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 40/41 changed files
- Comments generated: 6
- Review effort level: Balanced
Several pre-existing cases shared a name with another in the same file, so a failure could not be traced back to one of them. Renames only. Assisted-by: Claude Code:claude-opus-5[1m] Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QDmwmrXFKyZGJ3EdzTzmS7
…ltins with bash One place now expands an assignment and resolves its subscripts, instead of four that disagreed about them: an indexed subscript is arithmetic, an associative one a literal key. `declare`, `export`, `readonly` and `local` all route through it, so they agree with bash on array conversion, readonly refusals, bad subscripts, and compound values that stop at a bad key. Drops the `fancy-regex` dependency from brush-builtins; the parser now reads `name[index]=value`. BREAKING CHANGE: adds `ErrorKind` variants (the enum is not `non_exhaustive`); replaces `ShellVariable::convert_to_indexed_array` and `convert_to_associative_array` with `convert_to_array_kind`; `Shell::define_func` returns `Result`. Assisted-by: Claude Code:claude-opus-5[1m] Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QDmwmrXFKyZGJ3EdzTzmS7
`unset a[i]` carried its own copy of the subscript rule. It now uses the same resolver as everything else, so `a[*]`, `a[@]` and an empty subscript behave as in bash. A readonly variable or element is refused and reported without stopping the remaining names. BREAKING CHANGE: `Shell::undefine_func` returns `Result`, refusing a readonly function. Assisted-by: Claude Code:claude-opus-5[1m] Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QDmwmrXFKyZGJ3EdzTzmS7
BASH_COMMAND was rebuilt by joining the expanded arguments, so a DEBUG trap saw `echo hello` where bash shows `echo $greeting`. Pass the command's source text instead. Assisted-by: Claude Code:claude-opus-5[1m] Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QDmwmrXFKyZGJ3EdzTzmS7
`$(( ))` parsed as 0 but `$(( ))` did not: the empty rule matched only at end-of-input. Let it skip leading whitespace, so a blank expression is 0 wherever it comes from -- `$(( ))`, `let ''`, or a blank array subscript. Assisted-by: Claude Code:claude-opus-5[1m] Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QDmwmrXFKyZGJ3EdzTzmS7
…y their own builtins `declare.rs` held two unrelated jobs behind one options struct; the display half moves to `declare/display.rs`. `local` and `readonly` get their own command structs, so each accepts only the options it really takes. The interpreter's two assignment targets are named rather than passed as three loose booleans. Assisted-by: Claude Code:claude-opus-5[1m] Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QDmwmrXFKyZGJ3EdzTzmS7
`mod local`'s cfg attribute landed above `mod pushd`'s, gating each on the other's feature. `export` is implemented in terms of `declare`, so `mod declare` is compiled in for either feature -- rather than having `builtin.export` pull in `builtin.declare`, which would also register `declare`, `typeset`, `local` and `readonly` for a consumer that asked only for `export`. N.B. `builtin.dirs`, `builtin.exec`, `builtin.popd` and `builtin.pushd` still do not build alone; that predates this branch. Assisted-by: Claude Code:claude-opus-5[1m] Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U84rFy2TMZURarndduUe3f
… list Any failure on an unquoted compound operand became an assignment error, so an unimplemented case stopped the whole script instead of failing one command. Name the two classes on `ErrorKind` and have the callers ask. Refusals now name the variable they are about wherever they are raised, so `read`, `mapfile` and the arithmetic assignment operators report `r: readonly variable` rather than nothing useful. BREAKING CHANGE: `Error::source()` yields the kind's own source rather than the kind, so printing the chain no longer repeats the message; `EvalError::FailedToUpdateEnvironment` carries a `String`. Assisted-by: Claude Code:claude-opus-5[1m] Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QDmwmrXFKyZGJ3EdzTzmS7
Known failures for `local -g` (ignored; the declaration is always local), the function trace attribute (`declare -ft` is recorded and displayed but does not make a function inherit the DEBUG and RETURN traps), and the `+X` form of declare's mode options (rejected by the option parser). All pre-existing. Assisted-by: Claude Code:claude-opus-5[1m] Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QDmwmrXFKyZGJ3EdzTzmS7
bash 5.3 changed two behaviors these cases pin: a bad key in a compound value is now an assignment error rather than a skipped element, and a readonly variable now refuses the value-transforming attributes and any retyping. brush follows 5.3, so the cases fail against the 5.2 oracle on the aarch64 CI runner. Verified against bash 5.2.37 and 5.3.9; the nine are exactly the nine that failed on aarch64. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U84rFy2TMZURarndduUe3f
ff40111 to
395fef2
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Error chaining loses several underlying causes, while new API documentation contains private links and an advertised interface mismatch.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
brush-core/src/shell/expansion.rs:67
- This public rustdoc links to the crate-private
expansion::resolve_array_subscripthelper. Remove the private link (or deliberately expose the helper) so public documentation passes the deniedprivate_intra_doc_linkslint.
/// Resolves one array subscript against the kind of the array it names. See
/// [`expansion::resolve_array_subscript`].
brush-core/src/shell/expansion.rs:84
- This public rustdoc links to the crate-private
expansion::resolve_assignment_subscriptshelper, which can fail the workspace's denied private-link rustdoc lint. Keep this documentation self-contained instead.
/// Resolves the subscripts of an assignment whose words were already expanded, leaving its
/// values untouched. See [`expansion::resolve_assignment_subscripts`].
- Files reviewed: 40/41 changed files
- Comments generated: 3
- Review effort level: Balanced
…lpers The three `Shell` wrappers pointed at `expansion::expand_assignment` and friends, which are `pub(crate)`. Under the workspace's denied rustdoc lints that is `private_intra_doc_links`, so `cargo doc -p brush-core` failed. Keep the pointer as plain code text rather than a link. N.B. `OpenFile`'s link to its private `try_clone_to_owned` still fails the same way; that predates this branch. Assisted-by: Claude Code:claude-opus-5[1m] Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U84rFy2TMZURarndduUe3f
There was a problem hiding this comment.
🔵 Needs a closer look
Associative wildcard unsets are incorrect, and failed compound assignments can retain attributes that should be rolled back.
Review details
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
brush-builtins/src/declare/mod.rs:774
- The pre-assignment attributes are mutated before
stopped_byis turned into an assignment error, and the early return inupdate_variablenever rolls them back. Consequentlydeclare -i i=(1 []=2)leavesiinteger-typed even though the new compatibility case specifies that an unquoted compound refusal grants no option attributes; the same leak affects the case-transform, nameref, and trace flags. Apply these attributes only to temporary assignment state, then retain or restore them according to the final outcome.
brush-builtins/src/unset.rs:186 - This treats
*and@as “clear all” for every array kind. For an associative array, Bash instead treats them as literal keys, sodeclare -A m=(['*']=v); unset 'm[*]'must remove that element; the current branch callsunset_all_indices, whose associative case is a no-op. Restrict the clear-all path to indexed arrays and let associative arrays flow through normal key resolution/removal.
- Files reviewed: 40/41 changed files
- Comments generated: 0 new
- Review effort level: Balanced
…n key `unset 'a[*]'` clears an indexed array, and the resolver applied that to every array kind. An associative array can hold `*` or `@` as an ordinary key, so there the subscript names one element like any other key; bash removes it, while the clear-all path was a no-op and removed nothing. Assisted-by: Claude Code:claude-opus-5[1m] Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U84rFy2TMZURarndduUe3f
…eeds `-t` was applied in the pass that runs before the value is stored, alongside the attributes that shape how it is stored (`-i`, `-c`/`-l`/`-u`, `-n`). Those survive a failed assignment in bash, but `-t` does not: it shapes nothing, so it belongs with `-x` and `-r`, which an unquoted compound refusal rolls back. `declare -t v=(1 []=2)` left `v` traced where bash leaves it plain. Also records, as a known failure, that `declare -p` does not quote an associative key of `@` the way bash does. The key quoting predates this branch. Assisted-by: Claude Code:claude-opus-5[1m] Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U84rFy2TMZURarndduUe3f
There was a problem hiding this comment.
🟡 Changes recommended
Readonly empty-subscript unsets, failed compound attributes, and alias-expanded BASH_COMMAND remain incorrect.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
brush-core/src/interp.rs:1318
self.to_string()is the pre-alias AST, while alias expansion has already replaced the command innext_argsabove. Bash exposes the alias-expanded command inBASH_COMMAND, so withalias e='echo'; e hi, the DEBUG trap will seee hihere instead ofecho hi. Track source text after alias substitution but before ordinary word expansion rather than always using the original AST.
- Files reviewed: 40/41 changed files
- Comments generated: 2
- Review effort level: Balanced
The table's exception line read as though a failed unquoted compound operand left the variable untouched, but the attributes that shape how a value is stored are applied before the assignment and survive its refusal, matching bash. Name the distinction and point at the helper that already documents it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016vakRdMgWTRe53CSLfswaX
Assignment expansion lived in three inline helpers in
interp.rsand was applied uniformly, regardless of what the assignment targeted. That made two things impossible: a declaration builtin could not apply the subscript rules of the array type it was about to create, and the interpreter could not expand an operand's words at the moment a shell does — before the command runs.This centralizes the logic in
expansion.rsand splits it into the two passes a shell actually performs:Shell::resolve_assignment_subscripts), run by the declaration builtin once its options reveal the target array type — arithmetic for indexed arrays, ordinary word expansion for associative keys.Ordinary
name=valuestatements compose both in one call (Shell::expand_assignment).The ordering is the point: a shell expands an assignment's words before evaluating any arithmetic subscript, so side effects in a subscript are not visible to the value being assigned.
i=0; a[i++]=$inow stores0, as bash does, where it stored1.declare,export,local,readonlyandunsetall route through the shared resolver, so they agree with bash on array conversion, readonly refusals, bad subscripts, and compound values that stop at a bad key.Assignment and declaration
declare -a 'arr=(${X})',declare -a "arr=$(printf '(%s)' '${X}')", andcommand declare x=$vall work. Splitting at the right moment is what makesIFS=, declare -a arr=($X)keep the value whole whileIFS=, declare -a 'arr=(${X})'splits it.-a/-Aoverriding an existing type, sodeclare -A 'map[$key]=v'uses the literal key.+a/+Arefuse with a reason instead of an internal error, and a value merely ending in]is no longer read as a subscripted name.arr=(one); readonly arr; arr[0]=xsilently mutated the array — onmaintoo. The guard now sits where every element-assignment path converges, covering the declaration, arithmetic, assignment-expansion, andmapfilepaths.local -Iresolves subscripts against the variable it inherits, not the local about to shadow it, so an associative key is no longer arithmetically evaluated to0.a[0]=(9 9)reports "cannot assign list to array member" instead of "not yet implemented".-t(previously a copy-paste of the-rfilter) and lists declared-but-unassigned arrays.Which failures abandon the command list
An assignment error is a shell's own refusal to assign, and only that abandons the rest of the command list. Any failure on an unquoted compound operand used to qualify, so an unimplemented case stopped the whole script instead of failing one command.
ErrorKindnow names the two classes and callers ask.Refusals also name the variable they concern wherever they are raised, so
read,mapfileand the arithmetic assignment operators reportr: readonly variablerather than nothing useful.unsetunset a[i]carried its own copy of the subscript rule. It now uses the shared resolver, soa[*],a[@]and an empty subscript behave as in bash. A readonly variable or element is refused and reported without stopping the remaining names. Readonly functions are refused too.Also here
BASH_COMMANDreports the command as written rather than as expanded, so a DEBUG trap seesecho $greeting, notecho hello.$(( ))evaluates to 0. The empty-expression rule matched only at end of input, so$(( ))parsed but$(( ))did not. A blank expression is now 0 wherever it comes from —$(( )),let '', or a blank array subscript.declare.rsis split. The display half moves todeclare/display.rs;localandreadonlyget their own command structs, so each accepts only the options it really takes.mod local's cfg attribute had landed abovemod pushd's, gating each on the other's feature. (builtin.dirs,builtin.exec,builtin.popdandbuiltin.pushdstill do not build alone; that predates this branch.)fancy-regexis dropped frombrush-builtins; the parser now readsname[index]=value.API
brush-coregainsShell::{expand_assignment, resolve_assignment_subscripts, resolve_array_subscript},env::ShellEnvironment::{subscript_kind, unset_all_indices},expansion::ResolvedAssignment,variables::{ArrayKind, ScalarConversionPolicy, ShellValue::array_kind, ShellVariable::assign_at, ShellVariable::unset_all_indices},ExecutionContext::trace_extra_line, and readonly/trace accessors onfunctions::Registration.Breaking:
ErrorKindgains variants and is notnon_exhaustive.Error::source()yields the kind's own source rather than the kind, so printing the chain no longer repeats the message.EvalError::FailedToUpdateEnvironmentcarries aString.ShellVariable::convert_to_indexed_arrayandconvert_to_associative_arrayare replaced byconvert_to_array_kind.Shell::define_funcandShell::undefine_funcreturnResult, refusing a readonly function.Testing
Compat cases go from 2521 to 2691, and
known_failuremarkers from 482 to 435. Seventy-nine cases previously markedknown_failurenow pass — 56 indeclare, the rest acrossset -x,functrace,export,command,trap,readonlyandextdebug. No previously passing case regresses.Thirty-two of the new cases are recorded as known failures rather than fixed here, covering pre-existing gaps this work surfaced:
local -g, namerefs as assignment targets,declare's+Xmode options, the function trace attribute, field splitting inside a compound value, statuses reported from inside a pipeline, and an arithmetic error abandoning the rest of a;-separated list.Nine cases are annotated
min_oracle_version: "5.3". Bash 5.3 changed how a bad compound-array key and a readonly variable's attributes are handled; brush follows 5.3, and the aarch64 CI runner's oracle is 5.2.