Skip to content

Release Notes

brush-v0.4.0

chore: Release package brush version 0.4.0

brush v0.4.0

🚀 Release Notes

This is a significant release, containing several months of work and more than 200 merged PRs. It includes a broad set of new features, bug fixes, and quality improvements, most notably a myriad of bash compatibility fixes that materially raise the bar on which real-world scripts and interactive sessions brush can handle.

Some key highlights:

  • A meaningful step forward in bash compatibility. Major bash language features are now implemented or substantially expanded, e.g.: set -e, set -u, pipefail, failglob, the ERR trap, coprocesses, and a great deal more.
  • Improved robustness across edge cases. Closed pipes, broken stdout, unusual file-descriptor states, non-UTF8 history files, and platform corner cases are now handled gracefully. A systematic audit also removed an entire class of avoidable failure modes.
  • Broader platform support. Using brush as a login shell on macOS is now supported, Windows path handling is overhauled, FreeBSD, Android and 32-bit targets build cleanly again, and wasm32-wasip2 is now exercised in CI.
  • A more capable interactive shell. Optional TOML config, zsh-style preexec/precmd hooks, experimental terminal integration, expanded readline-macro support, and many completion improvements.
  • API improvements and foundations for what's next. Scaffolding for a winnow-based parser, a generic Shell<Extensions> for embedders, an opt-in bundled-coreutils build, and serde features for both AST and shell state. This work has required breaking changes to API surface, mostly to brush-core. Some changes were also made to brush-parser's API but we expect migration for parser-only consumers to be straightforward and relatively minimal. Please reach out to us if you encounter challenges in this area.

A heartfelt thank-you to everyone who filed issues, reproduced bugs on unusual platforms, and contributed code over the last several months 🙏. A significant percentage of the work below is in direct response to community reports.


✅ bash compatibility

This release expands brush's coverage of widely-used bash language features. Among the most commonly-relied-on additions:

  • set -e (errexit) and pipefail (#852) are implemented with bash-faithful semantics, including the long list of exemptions bash has accumulated over the years. See the compatibility reference for the current status of supported set options.
  • set -u (nounset) (#774, #1007) now actually errors on unset variable references, including the subtle interaction with ${#arr[i]}.
  • failglob (#1011) treats no-match globs as an error.
  • The ERR trap (#1020) is implemented, complementing the EXIT trap work from the previous release. As a corollary, EXIT traps now also fire correctly under -c (#1080).
  • Coprocesses (coproc) (#1029, #1068), both parsing and execution. See the compatibility reference.
  • A new --noglob/-f command-line flag (#1091), proper -- option-terminator handling for -c (#1076), and an exit code of 2 for unknown command-line options (#1050).

Heads up: because set -e, set -u, pipefail, and failglob previously had limited effect in brush, scripts that ran cleanly under v0.3.0 may now exit with errors that bash would also have produced. This is the intended behavior. If you see a divergence from bash, please file an issue.

Beyond the headline options, this release lands a long list of more focused compatibility fixes. Many are small, but in aggregate they account for the bulk of the change list. Highlights worth calling out:

  • Arithmetic got serious attention: high-radix literals (#1019), faithful overflow/underflow (#1017), legacy $[expr] syntax (#1004), the missing |= and ^= assignment operators (#911), array indices inside array indices (#910), arithmetic-for parsing/eval fixes (#1018), and infinite-recursion detection (#1021).
  • Heredocs are substantially more robust inside command substitutions and quoted contexts (#1014, #1055, #1056, #1067).
  • Pattern and extglob correctness work covers escaped parens (#960), backslash preservation (#963), pattern character-set escaping (#824), brace-sequence step direction (#1025), and the nullglob + quoted-empty-string interaction (#1035).
  • Builtins matured across the board: caller was added (#812), read is now much closer to feature-complete (#914), mapfile -O works (#558), getopts got OPTERR semantics (#1048) and short-option fixes (#827), compgen -A picked up binding, command, and reserved-word completion (#814, #1027, #1047), printf %q quotes empty strings as '' (#1026), and the rules for shadowing special builtins with functions are now correct (#922).
  • Variables and prompts: BASH_ARGC/BASH_ARGV (#1013) and $_ (#1030) are populated; OSTYPE is set on macOS (#1097); prompts handle \l, \[/\], and $? correctly (#913, #909, #798); and $- correctly reflects -c (#767).

If you've previously hit a bash-compatibility issue with brush, there's a decent chance it's resolved here. If not, please open an issue!


Robustness improvements

v0.4.0 also includes a substantial amount of plumbing work that won't show up in our feature lists but contributed to raising our quality. A big thanks to contributors who put these (and others) together:

  • A systematic unwrap() audit (#921) hardened brush against a class of unusual-input edge cases.
  • Closed stdout and stderr are now handled gracefully; this is important when brush is on the producing side of a | head pipeline (#873, #875).
  • File-descriptor failures (e.g., dup failures from clone) are now reported cleanly rather than aborting (#1051).
  • Path search now finds executables reachable via symlinks (#991, #992), broadening the set of environments where commands resolve as expected.
  • History import tolerates non-UTF8 and otherwise unusual entries (#878).
  • Regex-cache contention under concurrent use is reduced (#1043), with additional allocation-reduction work in brace expansion and other hot paths (#884, #886, #936, #940).

Broader platform support

We aim for portability and this release advances that goal on several fronts:

  • macOS: using brush as your login shell is now supported, thanks to a fix for a startup hang in that scenario (#1095). If you'd like to set brush as your chsh target on a Mac, please give it a try.
  • Windows: broad path-handling improvements (#1075), proper /dev/null emulation (#1044, #1104), and CI now exercises the Windows test suite (#1083) to keep the platform on solid footing. Windows support remains in an experimental state, but it has matured meaningfully this cycle. If you've tried brush on Windows previously, this is a good time to revisit, and we'd appreciate hearing how it goes.
  • FreeBSD builds again (#980).
  • Android and 32-bit targets build cleanly (#1070).
  • WebAssembly: split_paths is now safe under WASM (#1064), and basic wasm32-wasip2 smoke tests run in CI (#1098).

Interactive shell improvements

The interactive experience picked up several things that interactive-shell users have requested:

  • TOML configuration file for brush-shell: an opt-in alternative to shell-script-based configuration for brush-specific shell-level settings (#895). See the configuration reference for the file format, location, and available settings. Experimental.
  • preexec / precmd hooks in the style of zsh: useful for prompt frameworks, timing, integration tools (#652). Enable via the [experimental] zsh-hooks setting documented in the configuration reference. Experimental.
  • Shell/terminal integration: semantic prompt and command marking that modern terminals can use to enable command navigation, exit-status display, and similar features (#872). Enable via the [experimental] terminal-shell-integration setting in the configuration reference. Experimental.
  • Readline macros: initial support (#880) plus follow-up to handle more macro forms found in real inputrc files (#967). Many additional fixes to improve compatibility with shell extension tools like starship, atuin, fzf, and others.
  • Hint-acceptance key bindings are now exposed (#802).
  • A bag of completion fixes that mostly fall under "things you'd never notice unless they were broken": filenames with spaces and special characters now escape correctly (#870); shell variables get a sensible default fallback completion (#954); . and .. are offered (#887); mark-directories is honored (#817); COMP_KEY/COMP_TYPE are populated for completion functions (#1008); COMP_WORDBREAKS default matches bash (#828); word tokenizing during completion was corrected (#816); and deduplication of completions is deferred to the presentation layer so functions see the full list (#1012).
  • Profile/rc loading is now deferred until the input backend is attached (#879), which matters for rc files that probe terminal capabilities at startup.

For embedders and crate consumers

The brush crates are published to crates.io, and several of the changes in this release are aimed at people building on top of brush-core, brush-parser, et al. The most important ones:

  • Shell is now generic over a ShellExtensions type parameter (#941). This is the scaffolding that lets embedders extend brush with custom builtins, custom variable behavior, and other hooks without forking. Existing code that names Shell directly will need to either pick a concrete extension type or thread the generic through; a default extension type is provided for the common case. We plan to continue building on this for better low-overhead, static inspection and extension of brush-core behaviors.
  • Shell fields are now private (#900) and shell.rs was split into shell/*.rs (#945). Callers using the public re-exports and accessor/builder methods are unaffected; callers reaching into private state will need to migrate.
  • serde features are now available on both brush-parser (#783) and brush-core (#831) for AST and shell-state serialization. This makes a number of new use cases viable, e.g.: memoizing initialization, snapshotting shell state.
  • PEG parser reorganization (#899) along with scaffolding for a future winnow-based parser (#974). The PEG parser remains the production parser; the winnow work is groundwork for a faster, more diagnosable replacement in a future release.
  • brush-parser: ParsingNearToken was renamed to ParsingNear (#1022).
  • MSRV bumped to Rust 1.88 (#981).
  • fuzz-testing Cargo feature was renamed to arbitrary (#844) to better reflect what it actually gates.
  • Build/test/validate scripting moved to cargo xtask subcommands (#898); see cargo xtask --help.

Experimental work to keep an eye on

A few items in this release are intentionally marked experimental; they are usable today, but the shape of the API or UX may evolve based on feedback (hint: we want your feedback!):

  • Bundled coreutils builtins behind a feature flag (#1031). Opt in at build time to ship a single binary that resolves common utilities (ls, cat, etc.) internally, useful for container/embedded scenarios where a coreutils package isn't available, or for platforms like Windows that don't typically offer these utilities. See the experimental features reference.
  • Full shell-state serialization via the experimental save builtin (#835): primarily a debugging and tooling aid today, but a building block for future features. See the experimental features reference.
  • The TOML config, preexec/precmd, and terminal-integration features mentioned above are all in the experimental bucket. The experimental features reference covers how to enable each one. Try them and tell us what feels off.

Application-level breaking changes

If you're a script author or interactive user (rather than an embedder), these are the changes most likely to be visible to you:

  • Stricter set -e / set -u / pipefail / failglob enforcement. These options had limited effect in v0.3.0; they now behave like bash. Scripts that previously ran without surfacing errors may need updates.
  • readonly now operates on global scope (#1003), matching bash. Code that depended on the prior local behavior will observe a difference.
  • Reserved words are rejected as function names (#978).
  • Unknown command-line options exit with code 2 (#1050), matching bash.
  • Profile/rc loading happens after input backend attach (#879). Visible only to rc files that probed for terminal capabilities at startup.

🙏 Acknowledgments

Releases like this one don't happen without the people who file thoughtful issues, reproduce bugs on platforms the maintainers don't have access to, and send code. Thank you to everyone who contributed to this release! Whether your name appears below or you helped via an issue, a discussion, or a reproducing test case we value and count on your contribution.

Special thanks go to the contributors whose work shows up in many of the bullet points above:

  • @lu-zero - has been the driving force behind a multi-release effort to modernize brush's parser and improve bash compatibility from the ground up. The work landing in this release is laying the groundwork for substantially more significant parser, diagnostics, and compatibility improvements in upcoming release: enriched ASTs, source-location tracking, parser reorganization, and the early scaffolding for a winnow-based parser. Thank you for the sustained, foundational contributions.
  • @Elsie19 - focused, high-quality bash-compat fixes spread across builtins, expansion, and corner cases.
  • @oech3 - ongoing edge-case testing, exit-code validation, and bug finding.
  • @StudioLE - surgical heredoc / tokenizer fixes that resolved a cluster of long-standing parser bugs.
  • @takeshiD and @xtqqczze - repeated, well-targeted contributions across the codebase.

And thank you to everyone else who authored or co-authored a commit in this release:

If you contributed and aren't listed here, that's a bug and unintentional! Please let me know and I'll correct it.


The full change list

For the exhaustive, auto-generated list of every commit and PR since brush v0.3.0, see CHANGELOG.md.

brush-parser-v0.4.0

chore: Release package brush-parser version 0.4.0

brush-interactive-v0.4.0

chore: Release package brush-interactive version 0.4.0

brush-experimental-builtins-v0.1.0

chore: Release package brush-experimental-builtins version 0.1.0

brush-coreutils-builtins-v0.1.0

chore: Release package brush-coreutils-builtins version 0.1.0

brush-core-v0.5.0

chore: Release package brush-core version 0.5.0

brush-builtins-v0.2.0

chore: Release package brush-builtins version 0.2.0

brush-v0.3.0

chore: Release package brush version 0.3.0

brush v0.3.0

🚀 Release Notes

This is a major release, containing architectural improvements and compatibility-focused features and fixes. A big thanks to all of the contributors over the last few months, including issue filers, question-askers, and code contributors 🙏.

Based on user-filed issues (thank you!), targeted work went into improving compatibility with popular tools like fzf and neofetch.

Additionally, a significant amount of work went into refactoring and improving usability of the public API surface within the crate-based libraries that make up brush's core. These come with breaking changes for programmatic clients, but we're confident they will come with significant benefit.

The following summary covers the most notable changes and the full change details are below.

✅ Features and Improvements

Builtins

  • The fc builtin received its initial implementation!
  • bind implementation received support for binding key sequences to readline functions (e.g., "\C-a":beginning-of-line).
  • trap now fully implements the EXIT trap.

Standard variables

  • Basic support for use of BASH_XTRACEFD was added. A new command-line parameter to brush (--inherit-fd) enables allow-listing inherited file descriptors for use within the shell.

Control flow

  • Support for an undocumented C-like syntax for arithmetic for loops has been implemented for improved compatibility.

🪳 Notable bug fixes

  • Fixes for quote handling of \' in double-quoted word contexts.
  • Targeted fixes to history importing make it more robust against non-UTF8 compatible text.
  • Fixes for parsing of here-documents in quoted command substitutions.
  • Fixes for nuanced quote removal in parameter expressions with replacements (e.g., "${var:+'x'}").
  • Fixes for command substitution expansions resulting in spawning processes with large volumes of output (e.g., $(emit-a-lot-of-text)).
  • Updates to the open file table made by commandless exec (e.g., exec 2>/dev/null) are now reflected immediately, even when in a loop context.

🚧 API Changes

Changes in this section pertain to programmatic clients of the brush-core, brush-parser, and brush-interactive crates. They do not apply to application-level shell users.

brush-builtins

All shell builtins have been moved into a new dedicated brush-builtins crate. With this move, significant refactoring was done to ensure that these builtins are all implementable using public APIs exposed by brush-core. In doing so, this also significantly advanced the richness of the latter APIs.

Important

Programmatic clients of the brush-core crate will find that a constructed Shell is not automatically populated with the usual set of builtins. New optional APIs exist in the brush-builtins crate to retrieve default builtins and in brush-core to register them with a Shell instance.

Parsing and AST

As an important step on our journey to evolve brush's parsers, @lu_zero has made contributions that start enriching our parsed Abstract Syntax Trees (ASTs) with links back to the source text. As part of this work, a new SourceLocation trait was introduced and implemented for many AST node types. Work will continue in the coming releases.

API Ergonomics

To improve ergonomics, we've introduce builder-based construction utilities to key structures like brush_core::Shell. We will continue to expand on this adoption over time to simplify the work of constructing these values.

Note that the signature of Shell::new() has changed and may require adjustment in callers.

Error handling

As preparatory work for future improvements, we've overhauled the core error types and execution result types in brush-core:

  • Error has been renamed to ErrorKind, with a new wrapping Error struct type that can carry additional context. This will be used in the future for improved diagnostics.

  • Clients of brush-core can now provide their own custom error formatter to render a displayable string from an Error value. This will be used by some of our frontends for fancier diagnostics in the future. It also decouples error formatting from writing.

  • Multiple overlapping result types across the core interpreter, command utilities, and builtins have been consolidated into a coherent set of ExecutionResult, ExecutionExitCode, and ExecutionControlFlow types. Along the way a handful of bugs for edge cases were identified and resolved as part of the streamlining.

Details

  • build(deps): bump the github-actions group with 6 updates by @dependabot[bot] in #669
  • build(deps): bump procfs from 0.17.0 to 0.18.0 in the cargo group across 1 directory by @dependabot[bot] in #671
  • build(deps): bump the github-actions group with 5 updates by @dependabot[bot] in #675
  • Add a ShellBuilder by @lu-zero in #651
  • build(deps): bump the cargo group with 4 updates by @dependabot[bot] in #676
  • fix: clean cache in cd pipeline by @reubeno in #679
  • ci: run static code checks on linux + macOS too by @reubeno in #678
  • build(deps): bump serde from 1.0.221 to 1.0.223 in the cargo group by @dependabot[bot] in #680
  • build(deps): bump the github-actions group with 5 updates by @dependabot[bot] in #681
  • build(deps): bump the github-actions group with 2 updates by @dependabot[bot] in #683
  • build(deps): bump the cargo group with 5 updates by @dependabot[bot] in #684
  • chore: fix build error with cargo nightly by @reubeno in #687
  • build(deps): bump the github-actions group with 4 updates by @dependabot[bot] in #685
  • build(deps): bump the cargo group with 3 updates by @dependabot[bot] in #686
  • refactor: use Shell builder pattern in more code by @reubeno in #688
  • refactor: update Shell::new() to take creation options as owned by @reubeno in #689
  • ci: fix benchmark execution by @reubeno in #691
  • refactor: move builtins into their own crate by @reubeno in #690
  • refactor: Shell struct API improvements by @reubeno in #692
  • chore(msrv)!: upgrade MSRV to 1.87.0 by @reubeno in #693
  • refactor: os_pipe::pipe() -> std::io::pipe() by @reubeno in #695
  • chore: update dependencies by @reubeno in #696
  • build(deps): bump the github-actions group with 6 updates by @dependabot[bot] in #697
  • build(deps): bump bon from 3.7.2 to 3.8.0 in the cargo group by @dependabot[bot] in #698
  • build(deps): bump the github-actions group with 4 updates by @dependabot[bot] in #707
  • build(deps): bump the cargo group with 5 updates by @dependabot[bot] in #708
  • fix: workaround error on nightly by @reubeno in #711
  • fix: don't fail importing unreadable history lines by @reubeno in #710
  • refactor: extract script + function call stacks to their own modules by @reubeno in #709
  • docs: first draft of an AI policy in CONTRIBUTING.md by @reubeno in #694
  • build(deps): bump the github-actions group with 3 updates by @dependabot[bot] in #712
  • build(deps): bump the cargo group with 8 updates by @dependabot[bot] in #713
  • fix: tokenizer handling of here docs in quoted command substitutions by @reubeno in #716
  • build(deps): bump the github-actions group with 5 updates by @dependabot[bot] in #718
  • build(deps): bump the cargo group with 2 updates by @dependabot[bot] in #717
  • docs: add an initial AGENTS.md file by @reubeno in #714
  • refactor: error/result type overhaul by @reubeno in #720
  • feat: enable custom error formatting by @reubeno in #722
  • fix: address lint errors from stable + nightly by @reubeno in #723
  • chore: add instructions to guide advisory-only copilot code reviews by @reubeno in #721
  • ci: add cooldown settings for dependabot by @reubeno in #724
  • fix: add missing license symlink by @reubeno in #725
  • fix: add missing cfg conditions on builtin registrations by @reubeno in #726
  • Refactor SourcePosition to use Arc by @lu-zero in #727
  • Introduce source location by @lu-zero in #728
  • fix: correct read variable update behavior on empty input by @reubeno in #729
  • fix: address race conditions in basic input tests by @reubeno in #730
  • ci: upgrade bash-completion by @reubeno in #731
  • fix: do not pass along exported-but-unset vars by @reubeno in #732
  • fix: comment unsafe blocks + better harden 1 block by @reubeno in #733
  • ci: update cd workflow runners by @reubeno in #734
  • test: add not-yet-passing tests for set -u and set -e by @reubeno in #736
  • refactor: move more platform-specific code under sys by @reubeno in #735
  • feat: implement fc builtin by @reubeno in #739
  • feat(bind): extend key binding support by @reubeno in #740
  • docs: add fzf demo tape by @reubeno in #741
  • docs: update readme by @reubeno in #742
  • feat: implement alternate arithmetic for syntax by @reubeno in #744
  • fix(builtins): suppress error when type -p sees no command by @reubeno in #745
  • feat: implement BASH_XTRACEFD by @reubeno in #747
  • fix: command substitutions with large output by @reubeno in #748
  • feat: revisit how ExecutionParameters layer open files atop the Shell by @reubeno in #749
  • feat: basic support for trap EXIT by @reubeno in #750
  • fix: correct handling of expansion in parameter exprs with replacements by @reubeno in #751
  • test: add new command substitution test case by @reubeno in #752
  • build(deps): bump the github-actions group with 4 updates by @dependabot[bot] in #754
  • build(deps): bump the cargo group with 3 updates by @dependabot[bot] in #755
  • feat: implement opt-in fd inheritance from host env by @reubeno in #753
  • fix: correct expansion behavior in prompts by @reubeno in #756
  • chore: update dependencies by @reubeno in #757
  • chore: add brush crate + docs publishing by @reubeno in #760
  • fix: escaping and pipeline parse issues by @reubeno in #762
  • chore: prep release by @reubeno in #763
  • fix: correct changelog and versions by @reubeno in #764

Full Changelog: brush-shell-v0.2.23...brush-shell-v0.3.0