Skip to content

fix: support export -a NAME (declare-as-array + export); unblocks mise activate bash#1133

Draft
melderan wants to merge 1 commit into
reubeno:mainfrom
melderan:jmo-export-a-flag
Draft

fix: support export -a NAME (declare-as-array + export); unblocks mise activate bash#1133
melderan wants to merge 1 commit into
reubeno:mainfrom
melderan:jmo-export-a-flag

Conversation

@melderan

@melderan melderan commented May 4, 2026

Copy link
Copy Markdown

Summary

Implements export -a NAME (declare-as-array combined with export attribute), so the builtin no longer rejects the -a flag. This matches bash's behavior and unblocks mise activate bash, which emits export -a chpwd_functions early in its setup — without this fix, every mise-activated brush session prints error: unexpected argument '-a' found to stderr (mise still loads, but the chpwd_functions array isn't properly seeded).

Closes #1132.

Behavior

  • export -a NAME on an existing variable: convert to indexed array, mark exported
  • export -a NAME on a name that doesn't yet exist: create as unset indexed array with export attribute
  • export -a NAME=value (scalar): auto-promote to indexed array with [0]=value and mark exported, matching bash's declare -ax NAME=([0]="value")
  • export -a NAME=(a b c) (array literal): standard array initialization with export attribute
  • export -an NAME: combined with -n, removes the export attribute on an indexed array

Tests

Added five compat tests under brush-shell/tests/cases/compat/builtins/export.yaml:

  • export -a followed by element assignments (arr[0]=x; arr[1]=y)
  • export -a NAME=value scalar auto-promote
  • export -a NAME=(a b c) array literal
  • export -a converting an existing scalar to an indexed array
  • export -an removing the export attribute from an indexed array

Each is verified against bash via the compat oracle.

Known cosmetic divergence (documented, not fixed in this PR)

Bash is lazy about showing the -a attribute in declare -p for a declared-but-unassigned indexed array — it prints declare -x foo until the array is populated, then upgrades to declare -ax foo=([0]="..."). Brush is eager — it prints declare -ax foo immediately after export -a foo. The functional behavior is identical (the array is correctly typed and exported in both cases); only the declare -p representation for an empty declared array differs. The new tests therefore exercise the assignment paths, where bash and brush agree exactly. Happy to follow up with a "lazy attribute display" change if the maintainer wants exact declare -p parity, but it felt out of scope for this fix.

Local verification

  • cargo xtask ci pre-commit — fmt, build, lint, deps, schemas, unit tests all green; integration tests pass except for the pre-existing brush-interactive-tests::run_in_bg_then_fg flake unrelated to this change (also fails on main on this dev machine; the local interactive .bashrc references mise/direnv/atuin/zoxide hook functions that aren't present in CI).
  • mise activate bash | brush -c '. /dev/stdin && type mise' now succeeds with no stderr noise.

Filed as a draft for early input.


Assisted-by: Claude (Anthropic) (JMO's friend)

The `export` builtin now accepts `-a`, mirroring bash's combined
"declare as indexed array + mark exported" behavior. This is what
`mise activate bash` relies on when seeding `chpwd_functions` early
in its initialization, so before this change every mise-activated
brush session printed `error: unexpected argument '-a' found` to
stderr (mise still loaded, but the `chpwd_functions` array wasn't
properly set up).

Behavior:

- `export -a NAME` on an existing variable: convert it to an indexed
  array and mark it exported.
- `export -a NAME` on a name that doesn't yet exist: create it as
  an unset indexed array with the export attribute.
- `export -a NAME=value` (scalar): auto-promote to indexed array
  with `[0]=value` and mark exported, matching bash's `declare -ax`.
- `export -a NAME=(a b c)` (array literal): standard array
  initialization with the export attribute.
- `export -an NAME`: combined with `-n`, removes the export
  attribute on an indexed array.

Adds compat tests for each shape under
`brush-shell/tests/cases/compat/builtins/export.yaml`.

Note: bash is *lazy* about showing the `-a` attribute in
`declare -p` output for a declared-but-unassigned indexed array
(it shows `-x` until the array is populated, then upgrades to
`-ax`); brush is *eager* (shows `-ax` immediately after the
declaration). The functional behavior is identical — only the
cosmetic `declare -p` output for an empty declared array differs.
The new tests exercise the assignment paths, where bash and brush
agree exactly.

Closes reubeno#1132.

Assisted-by: Claude (Anthropic) (JMO's friend)
@reubeno

reubeno commented May 5, 2026

Copy link
Copy Markdown
Owner

Thanks for the contribution, @melderan!

We particularly appreciate the rest additions and clear example use case positively impacted by the improvement.

I've unblocked the CI checks to run. When you're ready for a formal review, please feel free to take this out of draft.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the export builtin to accept -a and apply “declare as indexed array” semantics while also setting/removing the export attribute, matching bash behavior needed by mise activate bash (e.g., export -a chpwd_functions).

Changes:

  • Add -a flag parsing to export and implement array promotion / array declaration + export behavior for both bare names and assignments.
  • Create missing variables as unset indexed arrays when export -a NAME is used.
  • Add compat test cases covering export -a with element assignment, scalar auto-promotion, array literal assignment, scalar→array conversion, and unexport behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
brush-builtins/src/export.rs Adds -a support to export, including array conversion and creating unset indexed arrays when needed.
brush-shell/tests/cases/compat/builtins/export.yaml Adds bash-oracle compat cases for the new export -a behavior.

Comment on lines +104 to +118
// If `-a` was passed and the name doesn't yet exist, create it as an unset
// indexed array with the export attribute (mirrors `declare -ax NAME`). This
// is what bash does for `export -a NAME` and what `mise activate bash` relies
// on when seeding `chpwd_functions`.
else if self.make_indexed_array {
let mut var =
ShellVariable::new(ShellValue::Unset(ShellValueUnsetType::IndexedArray));
if !self.unexport {
var.export();
}
context
.shell
.env_mut()
.add(s.clone(), var, EnvironmentScope::Global)?;
}
Comment on lines +143 to +145
// When `-a` is set, ensure the variable is converted to an indexed array
// before the assignment lands (so a scalar assignment like `export -a foo=x`
// ends up as `foo=([0]="x")`, matching bash's `declare -ax foo=([0]="x")`).
- name: "export -an unmarks an indexed array's export attribute"
stdin: |
export -a arr=(x y z)
export -n arr
@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown

Performance Benchmark Report

Benchmark name Baseline (μs) Test/PR (μs) Delta (μs) Delta %
clone_shell_object 17.34 μs 17.62 μs 0.28 μs 🟠 +1.60%
eval_arithmetic 0.15 μs 0.15 μs 0.00 μs ⚪ Unchanged
expand_one_string 1.58 μs 1.59 μs 0.01 μs ⚪ Unchanged
for_loop 30.68 μs 30.23 μs -0.46 μs 🟢 -1.49%
full_peg_complex 57.22 μs 56.48 μs -0.74 μs 🟢 -1.30%
full_peg_for_loop 6.21 μs 6.20 μs -0.01 μs ⚪ Unchanged
full_peg_nested_expansions 16.44 μs 16.85 μs 0.41 μs 🟠 +2.47%
full_peg_pipeline 4.27 μs 4.21 μs -0.06 μs 🟢 -1.47%
full_peg_simple 1.81 μs 1.78 μs -0.03 μs ⚪ Unchanged
function_call 3.35 μs 3.39 μs 0.04 μs ⚪ Unchanged
instantiate_shell 55.45 μs 55.41 μs -0.04 μs ⚪ Unchanged
instantiate_shell_with_init_scripts 27054.06 μs 27237.84 μs 183.78 μs ⚪ Unchanged
parse_peg_bash_completion 2095.66 μs 2095.63 μs -0.03 μs ⚪ Unchanged
parse_peg_complex 18.57 μs 19.48 μs 0.90 μs 🟠 +4.87%
parse_peg_for_loop 1.90 μs 1.86 μs -0.04 μs 🟢 -2.36%
parse_peg_pipeline 2.08 μs 2.04 μs -0.04 μs ⚪ Unchanged
parse_peg_simple 1.06 μs 1.08 μs 0.03 μs ⚪ Unchanged
run_echo_builtin_command 16.91 μs 17.00 μs 0.09 μs ⚪ Unchanged
tokenize_sample_script 3.49 μs 3.60 μs 0.11 μs 🟠 +3.06%

Code Coverage Report: Only Changed Files listed

Package Base Coverage New Coverage Difference
brush-builtins/src/export.rs 🟠 73.17% 🟢 77.55% 🟢 4.38%
brush-core/src/results.rs 🟢 80.16% 🟢 83.33% 🟢 3.17%
Overall Coverage 🟢 75.2% 🟢 75.24% 🟢 0.04%

Minimum allowed coverage is 70%, this run produced 75.24%

Test Summary: bash-completion test suite

Outcome Count Percentage
✅ Pass 1583 75.06
❗️ Error 17 0.81
❌ Fail 155 7.35
⏩ Skip 339 16.07
❎ Expected Fail 13 0.62
✔️ Unexpected Pass 2 0.09
📊 Total 2109 100.00

@reubeno

reubeno commented Jun 4, 2026

Copy link
Copy Markdown
Owner

Hi @melderan -- are you okay taking this out of draft for formal review? It was looking in relatively good shape CI-wise.

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.

bug: export -a NAME rejected (declare-as-array + export); breaks mise activate bash

3 participants