90 lines
5.4 KiB
Markdown
90 lines
5.4 KiB
Markdown
# 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](https://github.com/lu-zero/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](https://github.com/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:
|
|
|
|
```text
|
|
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.
|
|
- `Shell` is generic over `ShellExtensions`, 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 `sys` module, 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 implement `ParseArgs` through `brush-clap-adapter`, and those that parse
|
|
with winnow-args through `brush-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.
|
|
|
|
1. **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 from `PS1`, 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.
|
|
|
|
2. **Layers own their state.** A feature layered above the interpreter should reach
|
|
`Shell` only through public APIs, and keep its own state. Until `Shell` can hold state
|
|
owned by other crates, such features live in `brush-core`; don't add new coupling
|
|
between them and the rest of core.
|