5.4 KiB
Architecture
This describes how brush is split into crates, and the principles that decide which crate a piece of code belongs in. When a change raises the question "where should this go?", the answer should follow from here. If it doesn't, this document needs updating.
Crates
| Crate | Published | Role |
|---|---|---|
brush-parser |
yes | Tokenizes and parses shell syntax. Knows nothing about a running shell. |
brush-core |
yes | The shell itself: the Shell type, expansion, execution, jobs, variables, traps, and programmable completion. Has no line editor or UI of its own. |
brush-clap-adapter |
yes | Implements brush-core's engine-neutral builtin contract for builtins whose arguments are parsed with clap. |
brush-winnow-args-adapter |
yes | The same, for builtins whose arguments are parsed with winnow-args. |
brush-builtins |
yes | The standard builtins, registered on a Shell through brush-core's public API. |
brush-experimental-builtins |
yes | Builtins not ready to be standard. |
brush-coreutils-builtins |
yes | Optional builtins wrapping uutils/coreutils; doesn't depend on the other crates. |
brush-interactive |
yes | The interactive front end: reads input, edits the line (with reedline or a basic line reader), and presents prompts, completions, suggestions, and highlighting. |
brush-shell |
yes | The brush binary: its command line, and the wiring of the crates above into a shell. |
brush |
yes | A thin wrapper binary around brush-shell. |
brush-test-harness |
no | Runs the YAML test cases that compare brush with bash. |
brush-bundled-tests |
no | Tests the bundled-command shim end to end, against a brush with fake bundled commands. |
xtask, fuzz |
no | Development tooling. |
Their dependencies run one way, from front end to parser. Each crate's direct dependencies on the others (not counting dev-dependencies) are:
brush → brush-shell
brush-shell → brush-interactive, brush-builtins, brush-core,
brush-winnow-args-adapter,
brush-experimental-builtins (optional),
brush-coreutils-builtins (optional)
brush-interactive → brush-core, brush-parser
brush-builtins → brush-core, brush-parser, brush-winnow-args-adapter
brush-experimental-builtins → brush-core, brush-winnow-args-adapter
brush-clap-adapter → brush-core
brush-winnow-args-adapter → brush-core
brush-core → brush-parser
brush-parser (none)
brush-coreutils-builtins (none)
Anything a published crate exports is public API, including to people who embed brush without its front end. Keep that in mind when deciding what to export.
Structure within brush-core
- Shells are built with
Shell::builder(), a type-safe builder. Shellis generic overShellExtensions, which lets embedders swap in parts of its behavior (today, how errors are formatted, how processes are launched, and how the tasks the shell runs concurrently with itself are started) at compile time.- Platform-specific code lives under the
sysmodule, so the rest of the crate stays platform-neutral. - Builtins meet the shell at one contract,
brush_core::builtins, which names no argument-parsing engine. A builtin is a type that is its own parsed arguments: it parses an invocation's words (ParseArgs), supplies its help, and runs (Builtin), and is registered by type (Registration::of). Builtins that parse with clap implementParseArgsthroughbrush-clap-adapter, and those that parse with winnow-args throughbrush-winnow-args-adapter. - A shell keeps its builtins' registrations, by name, in a
builtins::Registry(Shell::builtins,Shell::builtins_mut), which is the one way to look them up, register them, or enable and disable them. How it stores them is private, so that can change (to share them between a shell and its subshells, say) without changing the API. - State a builtin keeps between invocations (as
getopts's place in its arguments) lives in the shell, kept by its type (ExecutionContext::state_mut), and is cloned with the shell, so it follows subshell rules as a variable does. A program embedding the shell can set it beforehand (Shell::set_builtin_state).
Layering principles
These decide where a behavior belongs when more than one crate could plausibly own it.
-
Meaning vs. presentation. If getting it wrong changes what the shell does -- what command a line runs, or what a script sees -- it generally belongs in
brush-core. If it only changes what the user sees, or how many keystrokes something takes, it belongs to the front end. For example, core composes a prompt fromPS1, and the front end draws it. A front end presents what core produces and chooses among it, but doesn't recreate shell semantics on its own.Shell semantics that a front end needs are exposed as core APIs rather than reimplemented. Helpers that only make sense inside one of core's own operations stay private.
-
Layers own their state. A feature layered above the interpreter should reach
Shellonly through public APIs, and keep its own state. UntilShellcan hold state owned by other crates, such features live inbrush-core; don't add new coupling between them and the rest of core.