Merge nucleic/nimble-umber-toad-20h7 into dev

This commit is contained in:
2026-08-11 20:11:26 -07:00
parent 959a80f5ec
commit 8e934d8996
16 changed files with 196 additions and 196 deletions
+15 -15
View File
@@ -2,42 +2,42 @@
Vendored copy of [reubeno/brush](https://github.com/reubeno/brush), pinned at Vendored copy of [reubeno/brush](https://github.com/reubeno/brush), pinned at
tag **`brush-shell-v0.4.0`** (upstream commit `96a26d0`), consumed by the tag **`brush-shell-v0.4.0`** (upstream commit `96a26d0`), consumed by the
`shell/` workspace to build **nash**, the Nucleic agent shell `shell/` workspace to build **hash**, the Hydrangea agent shell
([docs/NASH.md](../../docs/NASH.md)). ([docs/HYDRASHELL.md](../../docs/HYDRASHELL.md)).
Every fork change is marked with a `// nash:` comment. Current patches: Every fork change is marked with a `// hydrashell:` comment. Current patches:
| File | Change | | File | Change |
| --- | --- | | --- | --- |
| `brush-shell/src/productinfo.rs` | `PRODUCT_NAME` → `nash`; display string identifies the fork | | `brush-shell/src/productinfo.rs` | `PRODUCT_NAME` → `hydrashell`; display string identifies the fork |
| `brush-shell/src/args.rs` | usage/version strings rebranded | | `brush-shell/src/args.rs` | usage/version strings rebranded |
| `brush-core/src/prompt.rs` | `\$` (`PromptPiece::DollarOrPound`) renders the atom sign `⚛` (U+269B) for non-root instead of `$`; root still shows `#` | | `brush-core/src/prompt.rs` | `\$` (`PromptPiece::DollarOrPound`) renders the atom sign `⚛` (U+269B) for non-root instead of `$`; root still shows `#` |
| `brush-core/src/expansion.rs` | corpus divergence D1: added `ExpansionPiece::LiteralText` — literal unquoted text is never field-split (bash splits only expansion results) but keeps glob characters active. Parameter-expansion substitutions (`${v:-word}` etc.) convert back to splittable at the expansion boundary; `ExpanderOptions.field_split_literal_text` lets data-string callers opt in. Upstream candidate. | | `brush-core/src/expansion.rs` | corpus divergence D1: added `ExpansionPiece::LiteralText` — literal unquoted text is never field-split (bash splits only expansion results) but keeps glob characters active. Parameter-expansion substitutions (`${v:-word}` etc.) convert back to splittable at the expansion boundary; `ExpanderOptions.field_split_literal_text` lets data-string callers opt in. Upstream candidate. |
| `brush-core/src/completion.rs` | `compgen -W` word list opts into `field_split_literal_text` (the -W string is data) | | `brush-core/src/completion.rs` | `compgen -W` word list opts into `field_split_literal_text` (the -W string is data) |
| `brush-shell/tests/cases/compat/ifs.yaml` | removed `known_failure` from 3 IFS tests the D1 fix repairs | | `brush-shell/tests/cases/compat/ifs.yaml` | removed `known_failure` from 3 IFS tests the D1 fix repairs |
| `brush-builtins/src/dot.rs` + `brush-shell/tests/cases/compat/builtins/dot.yaml` | the `.` / `source` builtin searches `PATH` for slashless operands when `sourcepath` is enabled, as Bash/POSIX require; fixes `git-subtree` sourcing `git-sh-setup` | | `brush-builtins/src/dot.rs` + `brush-shell/tests/cases/compat/builtins/dot.yaml` | the `.` / `source` builtin searches `PATH` for slashless operands when `sourcepath` is enabled, as Bash/POSIX require; fixes `git-subtree` sourcing `git-sh-setup` |
| `brush-core/src/gate.rs` (new) + `lib.rs` | the `Gate` trait (docs/NASH.md §4.2): process-global, verdict-shaped hook; allow-all default so an unconfigured shell behaves exactly like upstream | | `brush-core/src/gate.rs` (new) + `lib.rs` | the `Gate` trait (docs/HYDRASHELL.md §4.2): process-global, verdict-shaped hook; allow-all default so an unconfigured shell behaves exactly like upstream |
| `brush-core/src/commands.rs` | `SimpleCommand::execute` split into gate wrapper + `execute_inner`: `on_exec` before dispatch (Deny short-circuits), completion correlated through all three spawn-result variants | | `brush-core/src/commands.rs` | `SimpleCommand::execute` split into gate wrapper + `execute_inner`: `on_exec` before dispatch (Deny short-circuits), completion correlated through all three spawn-result variants |
| `brush-core/src/processes.rs` | `ChildProcess.gate_token` + `on_exit` reporting from `wait()`/`poll()` (128+signal for signal deaths) | | `brush-core/src/processes.rs` | `ChildProcess.gate_token` + `on_exit` reporting from `wait()`/`poll()` (128+signal for signal deaths) |
| `brush-shell/src/entry.rs` | `set_exit_hook` seam so nash can flush its event buffer before `process::exit` | | `brush-shell/src/entry.rs` | `set_exit_hook` seam so hash can flush its event buffer before `process::exit` |
| `brush-core/src/gate.rs` | M2 (docs/NASH.md §5.2–5.3): `RedirectRecord`/`RedirectReadback` + `ExecStart.redirects` + `Gate::on_cmdsub` default hook | | `brush-core/src/gate.rs` | M2 (docs/HYDRASHELL.md §5.2–5.3): `RedirectRecord`/`RedirectReadback` + `ExecStart.redirects` + `Gate::on_cmdsub` default hook |
| `brush-core/src/interp.rs` | `ExecutionParameters.nash_redirects` accumulator; `setup_redirect` records file (`>`,`>>`,`<`,`2>`,`&>`,`<>`), heredoc, and here-string redirects | | `brush-core/src/interp.rs` | `ExecutionParameters.hydrashell_redirects` accumulator; `setup_redirect` records file (`>`,`>>`,`<`,`2>`,`&>`,`<>`), heredoc, and here-string redirects |
| `brush-core/src/commands.rs` | gate wrapper drains `nash_redirects` into `ExecStart` for post-run read-back | | `brush-core/src/commands.rs` | gate wrapper drains `hydrashell_redirects` into `ExecStart` for post-run read-back |
| `brush-core/src/expansion.rs` | command-substitution arm calls `gate().on_cmdsub` with the trimmed output | | `brush-core/src/expansion.rs` | command-substitution arm calls `gate().on_cmdsub` with the trimmed output |
| `brush-core/src/gate.rs` | pipe tap (docs/NASH.md §5.3): `PipeEvent` + `Gate::on_pipe` default hook + `is_active()`/`next_pipeline_id()` gating | | `brush-core/src/gate.rs` | pipe tap (docs/HYDRASHELL.md §5.3): `PipeEvent` + `Gate::on_pipe` default hook + `is_active()`/`next_pipeline_id()` gating |
| `brush-core/src/interp.rs` | `spawn_pipeline_processes` interposes a tee on each `a \| b` link when observation is active (`nash_spawn_pipe_tee`): a copier thread mirrors a bounded prefix, preserving backpressure and SIGPIPE, reports before closing the write end. No tee on the unobserved fast path. | | `brush-core/src/interp.rs` | `spawn_pipeline_processes` interposes a tee on each `a \| b` link when observation is active (`hydrashell_spawn_pipe_tee`): a copier thread mirrors a bounded prefix, preserving backpressure and SIGPIPE, reports before closing the write end. No tee on the unobserved fast path. |
Verification (this container): brush compat suite `2067 ran: 1684 succeeded, 6 Verification (this container): brush compat suite `2067 ran: 1684 succeeded, 6
failed, 377 known to fail, 38 skipped` — the 6 failures are identical to a failed, 377 known to fail, 38 skipped` — the 6 failures are identical to a
pristine `brush-shell-v0.4.0` baseline run (environmental: ANSI-C quote and pristine `brush-shell-v0.4.0` baseline run (environmental: ANSI-C quote and
`echo -e` oracle differences, syntax-error formatting, missing `hexdump`), i.e. `echo -e` oracle differences, syntax-error formatting, missing `hexdump`), i.e.
zero fork-caused regressions; +3 successes vs baseline are the repaired IFS zero fork-caused regressions; +3 successes vs baseline are the repaired IFS
tests. nash corpus: 94/94. tests. hash corpus: 94/94.
Planned (M3, per docs/NASH.md / NAROS.md): image-side delivery — nash baked and Planned (M3, per docs/HYDRASHELL.md / HYDRANGEAOS.md): image-side delivery — hash baked and
forced on every Linux surface via narOS. No further brush-core patches expected forced on every Linux surface via hydrangeaOS. No further brush-core patches expected
for M3; enforcement (`Hold`/`Deny` verdicts) remains post-M6. for M3; enforcement (`Hold`/`Deny` verdicts) remains post-M6.
Updating: diff against the upstream tag, re-vendor, re-apply `// nash:` patches Updating: diff against the upstream tag, re-vendor, re-apply `// hydrashell:` patches
(grep for the marker), then re-run `shell/corpus/replay.py` and the brush compat (grep for the marker), then re-run `shell/corpus/replay.py` and the brush compat
suite (`cargo test -p brush-shell --test brush-compat-tests`). suite (`cargo test -p brush-shell --test brush-compat-tests`).
+8 -8
View File
@@ -1,7 +1,7 @@
# nucleic-brush # nucleic-brush
A fork of [**brush**](https://github.com/reubeno/brush) — the Bo(u)rn(e) RUsty SHell by A fork of [**brush**](https://github.com/reubeno/brush) — the Bo(u)rn(e) RUsty SHell by
reuben olinsky — carrying the patches Nucleic needs to build **nash**, its agent shell. reuben olinsky — carrying the patches Nucleic needs to build **hash**, its agent shell.
Pinned at upstream tag **`brush-shell-v0.4.0`** (commit `96a26d0`). Pinned at upstream tag **`brush-shell-v0.4.0`** (commit `96a26d0`).
@@ -18,11 +18,11 @@ commands, their exits, and the data crossing pipes and redirections — from ins
shell rather than by wrapping it. That is what these patches provide. Everything else shell rather than by wrapping it. That is what these patches provide. Everything else
is upstream brush, unmodified. is upstream brush, unmodified.
Every divergence carries a `// nash:` comment, so the complete diff against upstream is Every divergence carries a `// hydrashell:` comment, so the complete diff against upstream is
one grep away: one grep away:
```console ```console
$ grep -rn "nash:" --include="*.rs" --include="*.yaml" . $ grep -rn "hydrashell:" --include="*.rs" --include="*.yaml" .
``` ```
### 1. The observation gate ### 1. The observation gate
@@ -65,12 +65,12 @@ and belong upstream:
### 3. Branding ### 3. Branding
The binary this workspace builds is still named `brush`, but identifies itself as nash The binary this workspace builds is still named `brush`, but identifies itself as hash
so an agent-facing shell doesn't misrepresent what it is: so an agent-facing shell doesn't misrepresent what it is:
| File | Change | | File | Change |
| --- | --- | | --- | --- |
| `brush-shell/src/productinfo.rs` | `PRODUCT_NAME` → `nash`; display string reads `nash (Nucleic agent shell, brush fork) …` | | `brush-shell/src/productinfo.rs` | `PRODUCT_NAME` → `hydrashell`; display string reads `hydrashell (hash — Hydrangea agent shell, brush fork) …` |
| `brush-shell/src/args.rs` | usage and version strings rebranded | | `brush-shell/src/args.rs` | usage and version strings rebranded |
| `brush-core/src/prompt.rs` | `\$` renders the atom sign `⚛` (U+269B) for non-root; root still shows `#` | | `brush-core/src/prompt.rs` | `\$` renders the atom sign `⚛` (U+269B) for non-root; root still shows `#` |
@@ -80,8 +80,8 @@ current verification status.
## How it's consumed ## How it's consumed
Nucleic's `shell/` workspace depends on `brush-shell` and `brush-parser` by path. The Nucleic's `shell/` workspace depends on `brush-shell` and `brush-parser` by path. The
`nash` binary is a thin wrapper that loads a root-owned operator policy, installs its `hydrashell` binary is a thin wrapper that loads a root-owned operator policy, installs its
`Gate` implementation (`nash-observe`), flushes events at exit, and otherwise defers `Gate` implementation (`hydrashell-observe`), flushes events at exit, and otherwise defers
entirely to brush's bash-compatible CLI. None of the fork's crates are published; the entirely to brush's bash-compatible CLI. None of the fork's crates are published; the
`brush` binary built from here is a development artifact. `brush` binary built from here is a development artifact.
@@ -105,7 +105,7 @@ ones a baseline run reproduces, plus the 3 IFS tests the expansion fix repairs.
1. Diff this tree against the pinned upstream tag. 1. Diff this tree against the pinned upstream tag.
2. Re-vendor the new tag. 2. Re-vendor the new tag.
3. Re-apply the fork patches — `grep -rn "nash:"` on the old tree enumerates all of 3. Re-apply the fork patches — `grep -rn "hydrashell:"` on the old tree enumerates all of
them. them.
4. Re-run the brush compat suite and Nucleic's transcript-replay corpus. 4. Re-run the brush compat suite and Nucleic's transcript-replay corpus.
5. Update the pin and the patch table in [`NUCLEIC_FORK.md`](NUCLEIC_FORK.md). 5. Update the pin and the patch table in [`NUCLEIC_FORK.md`](NUCLEIC_FORK.md).
+1 -1
View File
@@ -22,7 +22,7 @@ impl builtins::Command for DotCommand {
context: brush_core::ExecutionContext<'_, SE>, context: brush_core::ExecutionContext<'_, SE>,
) -> Result<brush_core::ExecutionResult, Self::Error> { ) -> Result<brush_core::ExecutionResult, Self::Error> {
// TODO(dot): Handle trap inheritance. // TODO(dot): Handle trap inheritance.
// nash: POSIX requires `.` to search PATH when its operand has no slash. // hydrashell: POSIX requires `.` to search PATH when its operand has no slash.
// Git's `git-subtree` relies on this with `. git-sh-setup`; treating the // Git's `git-subtree` relies on this with `. git-sh-setup`; treating the
// operand as cwd-relative leaves `require_work_tree` undefined. // operand as cwd-relative leaves `require_work_tree` undefined.
let script_path = Path::new(&self.script_path); let script_path = Path::new(&self.script_path);
+6 -6
View File
@@ -298,16 +298,16 @@ struct InputReader {
deadline: Option<Instant>, deadline: Option<Instant>,
/// Single-byte read buffer. /// Single-byte read buffer.
/// ///
/// nash(D2): multi-byte UTF-8 sequences are buffered and decoded incrementally in /// hydrashell(D2): multi-byte UTF-8 sequences are buffered and decoded incrementally in
/// `read_event` (up to 4 bytes via `std::str::from_utf8`); invalid sequences degrade /// `read_event` (up to 4 bytes via `std::str::from_utf8`); invalid sequences degrade
/// to per-byte Latin-1 chars (the pre-fix behavior for every byte). bash's `-n` /// to per-byte Latin-1 chars (the pre-fix behavior for every byte). bash's `-n`
/// counts bytes, which is preserved because the char-limit check measures /// counts bytes, which is preserved because the char-limit check measures
/// `line.len()` — the accumulated String's UTF-8 byte length. /// `line.len()` — the accumulated String's UTF-8 byte length.
buffer: [u8; 1], buffer: [u8; 1],
/// nash(D2): a byte consumed while expecting a UTF-8 continuation that turned out to /// hydrashell(D2): a byte consumed while expecting a UTF-8 continuation that turned out to
/// start the next character; re-consumed before the stream is read again. /// start the next character; re-consumed before the stream is read again.
pushback: Option<u8>, pushback: Option<u8>,
/// nash(D2): decoded-but-undelivered chars from an invalid sequence flushed as /// hydrashell(D2): decoded-but-undelivered chars from an invalid sequence flushed as
/// Latin-1; drained before the stream is read again. /// Latin-1; drained before the stream is read again.
pending: VecDeque<char>, pending: VecDeque<char>,
/// Terminal mode guard - kept alive for RAII cleanup on drop. /// Terminal mode guard - kept alive for RAII cleanup on drop.
@@ -356,7 +356,7 @@ impl InputReader {
brush_core::sys::poll::poll_for_input(&self.input, Duration::ZERO).unwrap_or(false) brush_core::sys::poll::poll_for_input(&self.input, Duration::ZERO).unwrap_or(false)
} }
/// nash(D2): reads one byte, honoring the pushback slot and the read deadline. /// hydrashell(D2): reads one byte, honoring the pushback slot and the read deadline.
/// Returns `Ok(None)` on EOF; timeout is reported via `TimedOut` in errors' stead by /// Returns `Ok(None)` on EOF; timeout is reported via `TimedOut` in errors' stead by
/// the caller checking the deadline, so this only polls when a deadline exists. /// the caller checking the deadline, so this only polls when a deadline exists.
fn read_byte(&mut self) -> Result<Option<u8>, brush_core::Error> { fn read_byte(&mut self) -> Result<Option<u8>, brush_core::Error> {
@@ -372,10 +372,10 @@ impl InputReader {
/// Reads the next input event, handling timeout and control characters. /// Reads the next input event, handling timeout and control characters.
/// ///
/// nash(D2): decodes UTF-8 incrementally instead of casting each byte to `char` /// hydrashell(D2): decodes UTF-8 incrementally instead of casting each byte to `char`
/// (which was a Latin-1 decode and re-encoded input bytes — divergence D2 in /// (which was a Latin-1 decode and re-encoded input bytes — divergence D2 in
/// shell/corpus/DIVERGENCES.md, found when ca-certificates' postinst mangled a /// shell/corpus/DIVERGENCES.md, found when ca-certificates' postinst mangled a
/// UTF-8 filename under narOS). Valid multi-byte sequences round-trip /// UTF-8 filename under hydrangeaOS). Valid multi-byte sequences round-trip
/// byte-identically through the accumulated String; invalid sequences degrade to /// byte-identically through the accumulated String; invalid sequences degrade to
/// the pre-fix per-byte Latin-1 mapping (a String cannot hold raw invalid bytes — /// the pre-fix per-byte Latin-1 mapping (a String cannot hold raw invalid bytes —
/// full byte-fidelity for invalid UTF-8 would need Vec<u8> plumbing shell-wide). /// full byte-fidelity for invalid UTF-8 would need Vec<u8> plumbing shell-wide).
+7 -7
View File
@@ -349,7 +349,7 @@ impl<'a, SE: extensions::ShellExtensions> SimpleCommand<'a, SE> {
/// executed command. This function's implementation is responsible for /// executed command. This function's implementation is responsible for
/// dispatching it appropriately according to the context provided. /// dispatching it appropriately according to the context provided.
/// ///
/// nash: this is the gate choke point (docs/NASH.md §4.2). Every simple /// hydrashell: this is the gate choke point (docs/HYDRASHELL.md §4.2). Every simple
/// command passes through the process-global [`crate::gate::Gate`] before /// command passes through the process-global [`crate::gate::Gate`] before
/// dispatch; completion is reported back with the issued token, however the /// dispatch; completion is reported back with the issued token, however the
/// command runs (inline, spawned process, or spawned task). /// command runs (inline, spawned process, or spawned task).
@@ -362,13 +362,13 @@ impl<'a, SE: extensions::ShellExtensions> SimpleCommand<'a, SE> {
if argv.is_empty() { if argv.is_empty() {
argv.push(self.command_name.clone()); argv.push(self.command_name.clone());
} }
// nash: hand this command's redirects to the observer for post-run read-back. // hydrashell: hand this command's redirects to the observer for post-run read-back.
let redirects = std::mem::take(&mut self.params.nash_redirects); let redirects = std::mem::take(&mut self.params.hydrashell_redirects);
let start_event = crate::gate::ExecStart { let start_event = crate::gate::ExecStart {
argv, argv,
cwd: self.shell.working_dir().to_path_buf(), cwd: self.shell.working_dir().to_path_buf(),
redirects, redirects,
pipeline: self.params.nash_pipeline, pipeline: self.params.hydrashell_pipeline,
}; };
let token = match gate.on_exec(start_event) { let token = match gate.on_exec(start_event) {
crate::gate::Verdict::Allow(token) => token, crate::gate::Verdict::Allow(token) => token,
@@ -836,11 +836,11 @@ pub(crate) async fn invoke_command_in_subshell_and_get_output(
// Get our own set of parameters we can customize and use. // Get our own set of parameters we can customize and use.
let mut params = params.clone(); let mut params = params.clone();
params.process_group_policy = ProcessGroupPolicy::SameProcessGroup; params.process_group_policy = ProcessGroupPolicy::SameProcessGroup;
// nash: a command substitution is an *argument* to the stage, not the stage itself, and it // hydrashell: a command substitution is an *argument* to the stage, not the stage itself, and it
// runs during expansion — i.e. before the stage's own command. Letting it inherit the slot // runs during expansion — i.e. before the stage's own command. Letting it inherit the slot
// made `grep "$(printf x)" f | wc -l` report `printf x` as the producing stage // made `grep "$(printf x)" f | wc -l` report `printf x` as the producing stage
// (docs/NASH.md §5.1). // (docs/HYDRASHELL.md §5.1).
params.nash_pipeline = None; params.hydrashell_pipeline = None;
// Set up pipe so we can read the output. // Set up pipe so we can read the output.
let (reader, writer) = std::io::pipe()?; let (reader, writer) = std::io::pipe()?;
+1 -1
View File
@@ -308,7 +308,7 @@ impl Spec {
// and field splitting but NOT pathname expansion (globbing). // and field splitting but NOT pathname expansion (globbing).
let options = crate::expansion::ExpanderOptions { let options = crate::expansion::ExpanderOptions {
pathname_expand: false, pathname_expand: false,
// nash: the -W string is data — its literal text is split too. // hydrashell: the -W string is data — its literal text is split too.
field_split_literal_text: true, field_split_literal_text: true,
..Default::default() ..Default::default()
}; };
+14 -14
View File
@@ -37,7 +37,7 @@ pub(crate) struct ExpanderOptions {
/// Whether to perform pathname expansion (globbing). If disabled, glob patterns /// Whether to perform pathname expansion (globbing). If disabled, glob patterns
/// are returned as literal strings. /// are returned as literal strings.
pub pathname_expand: bool, pub pathname_expand: bool,
// nash: whether literal (unquoted, non-expansion) text is also subject to // hydrashell: whether literal (unquoted, non-expansion) text is also subject to
// field splitting. Off by default per POSIX/bash (only expansion results // field splitting. Off by default per POSIX/bash (only expansion results
// split); enabled by callers whose input string is *data*, e.g. the // split); enabled by callers whose input string is *data*, e.g. the
// `compgen -W` word list. // `compgen -W` word list.
@@ -275,7 +275,7 @@ impl From<String> for WordField {
enum ExpansionPiece { enum ExpansionPiece {
Unsplittable(String), Unsplittable(String),
Splittable(String), Splittable(String),
// nash: literal unquoted word text. Bash field-splits only the *results of // hydrashell: literal unquoted word text. Bash field-splits only the *results of
// expansions*, never literal text — but glob characters in literal text stay // expansions*, never literal text — but glob characters in literal text stay
// active. This third state fixes `IFS=,; echo a,b,c` printing `a b c` // active. This third state fixes `IFS=,; echo a,b,c` printing `a b c`
// (corpus divergence D1) without breaking `echo *.txt`. // (corpus divergence D1) without breaking `echo *.txt`.
@@ -297,7 +297,7 @@ impl From<ExpansionPiece> for patterns::PatternPiece {
match piece { match piece {
ExpansionPiece::Unsplittable(s) => Self::Literal(s), ExpansionPiece::Unsplittable(s) => Self::Literal(s),
ExpansionPiece::Splittable(s) => Self::Pattern(s), ExpansionPiece::Splittable(s) => Self::Pattern(s),
// nash: unquoted literal text keeps glob chars active. // hydrashell: unquoted literal text keeps glob chars active.
ExpansionPiece::LiteralText(s) => Self::Pattern(s), ExpansionPiece::LiteralText(s) => Self::Pattern(s),
} }
} }
@@ -308,7 +308,7 @@ impl From<ExpansionPiece> for crate::regex::RegexPiece {
match piece { match piece {
ExpansionPiece::Unsplittable(s) => Self::Literal(s), ExpansionPiece::Unsplittable(s) => Self::Literal(s),
ExpansionPiece::Splittable(s) => Self::Pattern(s), ExpansionPiece::Splittable(s) => Self::Pattern(s),
// nash: unquoted literal text keeps pattern chars active. // hydrashell: unquoted literal text keeps pattern chars active.
ExpansionPiece::LiteralText(s) => Self::Pattern(s), ExpansionPiece::LiteralText(s) => Self::Pattern(s),
} }
} }
@@ -335,12 +335,12 @@ impl ExpansionPiece {
match self { match self {
Self::Unsplittable(_) => self, Self::Unsplittable(_) => self,
Self::Splittable(s) => Self::Unsplittable(s), Self::Splittable(s) => Self::Unsplittable(s),
// nash: quoting literal text also deactivates its glob chars. // hydrashell: quoting literal text also deactivates its glob chars.
Self::LiteralText(s) => Self::Unsplittable(s), Self::LiteralText(s) => Self::Unsplittable(s),
} }
} }
// nash: opt literal text into field splitting (see // hydrashell: opt literal text into field splitting (see
// `ExpanderOptions::field_split_literal_text`). Quoted pieces stay intact. // `ExpanderOptions::field_split_literal_text`). Quoted pieces stay intact.
fn make_literal_splittable(self) -> Self { fn make_literal_splittable(self) -> Self {
match self { match self {
@@ -547,7 +547,7 @@ struct WordExpander<'a, SE: extensions::ShellExtensions> {
in_double_quotes: bool, in_double_quotes: bool,
/// Whether to use heredoc expansion semantics (literal quotes, no brace expansion). /// Whether to use heredoc expansion semantics (literal quotes, no brace expansion).
heredoc_mode: bool, heredoc_mode: bool,
// nash: see `ExpanderOptions::field_split_literal_text`. // hydrashell: see `ExpanderOptions::field_split_literal_text`.
field_split_literal_text: bool, field_split_literal_text: bool,
} }
@@ -684,7 +684,7 @@ impl<'a, SE: extensions::ShellExtensions> WordExpander<'a, SE> {
&['$', '`', '\\', '\'', '\"', '~', '{'] &['$', '`', '\\', '\'', '\"', '~', '{']
}; };
if !word.contains(expansion_chars) { if !word.contains(expansion_chars) {
// nash: a word with no expansion characters is pure literal text — // hydrashell: a word with no expansion characters is pure literal text —
// glob-active but not subject to field splitting. // glob-active but not subject to field splitting.
return Ok(Expansion::from(ExpansionPiece::LiteralText( return Ok(Expansion::from(ExpansionPiece::LiteralText(
word.to_owned(), word.to_owned(),
@@ -821,7 +821,7 @@ impl<'a, SE: extensions::ShellExtensions> WordExpander<'a, SE> {
// Go through the fields we have so far. // Go through the fields we have so far.
for existing_field in expansion.fields { for existing_field in expansion.fields {
for piece in existing_field.0 { for piece in existing_field.0 {
// nash: callers whose input is data (e.g. compgen -W) opt // hydrashell: callers whose input is data (e.g. compgen -W) opt
// literal text into splitting. // literal text into splitting.
let piece = if self.field_split_literal_text { let piece = if self.field_split_literal_text {
piece.make_literal_splittable() piece.make_literal_splittable()
@@ -829,7 +829,7 @@ impl<'a, SE: extensions::ShellExtensions> WordExpander<'a, SE> {
piece piece
}; };
match piece { match piece {
// nash: literal text is never field-split (only expansion // hydrashell: literal text is never field-split (only expansion
// results are), but stays a distinct piece so its glob // results are), but stays a distinct piece so its glob
// characters remain active downstream. // characters remain active downstream.
ExpansionPiece::Unsplittable(_) | ExpansionPiece::LiteralText(_) => { ExpansionPiece::Unsplittable(_) | ExpansionPiece::LiteralText(_) => {
@@ -913,7 +913,7 @@ impl<'a, SE: extensions::ShellExtensions> WordExpander<'a, SE> {
) -> Result<Expansion, error::Error> { ) -> Result<Expansion, error::Error> {
let expansion: Expansion = match word_piece { let expansion: Expansion = match word_piece {
brush_parser::word::WordPiece::Text(s) => { brush_parser::word::WordPiece::Text(s) => {
// nash: literal word text — not field-split, glob-active. // hydrashell: literal word text — not field-split, glob-active.
Expansion::from(ExpansionPiece::LiteralText(s)) Expansion::from(ExpansionPiece::LiteralText(s))
} }
brush_parser::word::WordPiece::SingleQuotedText(s) => { brush_parser::word::WordPiece::SingleQuotedText(s) => {
@@ -963,7 +963,7 @@ impl<'a, SE: extensions::ShellExtensions> WordExpander<'a, SE> {
ExpansionPiece::Unsplittable(self.expand_tilde_expression(&tilde_expr)?), ExpansionPiece::Unsplittable(self.expand_tilde_expression(&tilde_expr)?),
), ),
brush_parser::word::WordPiece::ParameterExpansion(p) => { brush_parser::word::WordPiece::ParameterExpansion(p) => {
// nash: whatever a parameter expansion substitutes (including // hydrashell: whatever a parameter expansion substitutes (including
// literal text from ${v:-default} / ${v:+alt} words) is an // literal text from ${v:-default} / ${v:+alt} words) is an
// expansion *result*, so it is subject to field splitting. // expansion *result*, so it is subject to field splitting.
// Quoted pieces inside remain Unsplittable. // Quoted pieces inside remain Unsplittable.
@@ -999,7 +999,7 @@ impl<'a, SE: extensions::ShellExtensions> WordExpander<'a, SE> {
let trimmed_len = cmd_output.trim_end_matches('\n').len(); let trimmed_len = cmd_output.trim_end_matches('\n').len();
cmd_output.truncate(trimmed_len); cmd_output.truncate(trimmed_len);
// nash: capture command-substitution output (docs/NASH.md §5.3) — // hydrashell: capture command-substitution output (docs/HYDRASHELL.md §5.3) —
// invisible to the transcript, often carries decisions/secrets. // invisible to the transcript, often carries decisions/secrets.
if !self.disable_command_substitutions { if !self.disable_command_substitutions {
crate::gate::gate().on_cmdsub(cmd_output.as_str()); crate::gate::gate().on_cmdsub(cmd_output.as_str());
@@ -1012,7 +1012,7 @@ impl<'a, SE: extensions::ShellExtensions> WordExpander<'a, SE> {
// If we are *not* in a double-quoted context and we were requested to skip // If we are *not* in a double-quoted context and we were requested to skip
// unquoted backslash removal, then we need to skip removing backslashes here. // unquoted backslash removal, then we need to skip removing backslashes here.
if !self.in_double_quotes && self.disable_unquoted_backslash_removal { if !self.in_double_quotes && self.disable_unquoted_backslash_removal {
// nash: retained-backslash text is literal, not an // hydrashell: retained-backslash text is literal, not an
// expansion result — no field splitting. // expansion result — no field splitting.
return Ok(Expansion::from(ExpansionPiece::LiteralText(s))); return Ok(Expansion::from(ExpansionPiece::LiteralText(s)));
} }
+14 -14
View File
@@ -1,9 +1,9 @@
//! nash: the observation/enforcement gate (docs/NASH.md §4.2). //! hydrashell: the observation/enforcement gate (docs/HYDRASHELL.md §4.2).
//! //!
//! A process-global, verdict-shaped hook at the shell's command choke point. //! A process-global, verdict-shaped hook at the shell's command choke point.
//! The default implementation allows everything and observes nothing, so an //! The default implementation allows everything and observes nothing, so an
//! unconfigured shell behaves exactly like upstream brush. nash installs a //! unconfigured shell behaves exactly like upstream brush. hash installs a
//! recording gate at startup (`nash-observe`); a future enforcement mode may //! recording gate at startup (`hydrashell-observe`); a future enforcement mode may
//! return `Deny` (and, later, hold pending a host policy round-trip) without //! return `Deny` (and, later, hold pending a host policy round-trip) without
//! any further changes to this crate. //! any further changes to this crate.
@@ -19,15 +19,15 @@ pub struct ExecStart {
pub argv: Vec<String>, pub argv: Vec<String>,
/// The shell's working directory at execution time. /// The shell's working directory at execution time.
pub cwd: PathBuf, pub cwd: PathBuf,
/// nash: the redirections applied to this command (docs/NASH.md §5.2). The /// hydrashell: the redirections applied to this command (docs/HYDRASHELL.md §5.2). The
/// observer reads back the affected byte range after the command completes. /// observer reads back the affected byte range after the command completes.
pub redirects: Vec<RedirectRecord>, pub redirects: Vec<RedirectRecord>,
/// nash: the pipeline stage this command occupies, when it runs as part of /// hydrashell: the pipeline stage this command occupies, when it runs as part of
/// `a | b | c` (docs/NASH.md §5.1). `None` for a standalone command. /// `a | b | c` (docs/HYDRASHELL.md §5.1). `None` for a standalone command.
pub pipeline: Option<PipelineSlot>, pub pipeline: Option<PipelineSlot>,
} }
/// Where a command sits in the pipeline it belongs to (docs/NASH.md §5.1). /// Where a command sits in the pipeline it belongs to (docs/HYDRASHELL.md §5.1).
/// ///
/// Shares its `id` with the [`PipeEvent`]s of the same pipeline, which is what /// Shares its `id` with the [`PipeEvent`]s of the same pipeline, which is what
/// lets an observer name the commands either side of a link (`curl … | sh`) /// lets an observer name the commands either side of a link (`curl … | sh`)
@@ -42,7 +42,7 @@ pub struct PipelineSlot {
pub len: usize, pub len: usize,
} }
/// How a redirection's data is captured after the command runs (docs/NASH.md §5.2). /// How a redirection's data is captured after the command runs (docs/HYDRASHELL.md §5.2).
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RedirectReadback { pub enum RedirectReadback {
/// A truncating write (`>`, `>|`, `&>`): read the head of the final file. /// A truncating write (`>`, `>|`, `&>`): read the head of the final file.
@@ -55,7 +55,7 @@ pub enum RedirectReadback {
Inline, Inline,
} }
/// One redirection observed on a command. For regular files nash hands the child /// One redirection observed on a command. For regular files hash hands the child
/// the *real* fd (semantics untouched) and reads back a bounded range afterward; /// the *real* fd (semantics untouched) and reads back a bounded range afterward;
/// for heredoc/here-string the body is captured inline at setup. /// for heredoc/here-string the body is captured inline at setup.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@@ -81,7 +81,7 @@ pub struct ExecEnd {
pub exit_code: i32, pub exit_code: i32,
} }
/// Data observed flowing across one pipe link `a | b` (docs/NASH.md §5.3). /// Data observed flowing across one pipe link `a | b` (docs/HYDRASHELL.md §5.3).
/// Reported once the link drains (producer closed or consumer went away). /// Reported once the link drains (producer closed or consumer went away).
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct PipeEvent { pub struct PipeEvent {
@@ -133,10 +133,10 @@ pub trait Gate: Send + Sync {
/// Called when a simple command completes, with the token issued by /// Called when a simple command completes, with the token issued by
/// `on_exec`. /// `on_exec`.
fn on_exit(&self, token: u64, ev: ExecEnd); fn on_exit(&self, token: u64, ev: ExecEnd);
/// nash: called with the (trimmed) output of a command substitution /// hydrashell: called with the (trimmed) output of a command substitution
/// `$(…)` / backticks (docs/NASH.md §5.3). Default no-op. /// `$(…)` / backticks (docs/HYDRASHELL.md §5.3). Default no-op.
fn on_cmdsub(&self, _output: &str) {} fn on_cmdsub(&self, _output: &str) {}
/// nash: called once a pipe link `a | b` drains (docs/NASH.md §5.3). /// hydrashell: called once a pipe link `a | b` drains (docs/HYDRASHELL.md §5.3).
/// Invoked from the tee copier thread. Default no-op. /// Invoked from the tee copier thread. Default no-op.
fn on_pipe(&self, _ev: PipeEvent) {} fn on_pipe(&self, _ev: PipeEvent) {}
} }
@@ -165,7 +165,7 @@ pub(crate) fn gate() -> &'static dyn Gate {
} }
/// Whether a real (non-default) gate is installed. Pipe teeing — which adds a /// Whether a real (non-default) gate is installed. Pipe teeing — which adds a
/// thread + extra pipe per link — is only done when this is true (docs/NASH.md §5.3). /// thread + extra pipe per link — is only done when this is true (docs/HYDRASHELL.md §5.3).
pub(crate) fn is_active() -> bool { pub(crate) fn is_active() -> bool {
GATE.get().is_some() GATE.get().is_some()
} }
+111 -111
View File
@@ -37,16 +37,16 @@ pub struct ExecutionParameters {
/// Whether `errexit` (exit on error) behavior should be /// Whether `errexit` (exit on error) behavior should be
/// suppressed in this execution context. Defaults to `false`. /// suppressed in this execution context. Defaults to `false`.
pub suppress_errexit: bool, pub suppress_errexit: bool,
// nash: redirections applied to the command being built, drained into its // hydrashell: redirections applied to the command being built, drained into its
// `gate::ExecStart` for post-run read-back (docs/NASH.md §5.2). Empty unless // `gate::ExecStart` for post-run read-back (docs/HYDRASHELL.md §5.2). Empty unless
// observation is active and the command has redirects. // observation is active and the command has redirects.
pub(crate) nash_redirects: Vec<crate::gate::RedirectRecord>, pub(crate) hydrashell_redirects: Vec<crate::gate::RedirectRecord>,
// nash: the pipeline stage the command being built belongs to, stamped onto // hydrashell: the pipeline stage the command being built belongs to, stamped onto
// its `gate::ExecStart` so the observer can name the stages either side of a // its `gate::ExecStart` so the observer can name the stages either side of a
// pipe link (docs/NASH.md §5.1). Inherited by nested commands — a command // pipe link (docs/HYDRASHELL.md §5.1). Inherited by nested commands — a command
// inside a stage's subshell or brace group belongs to that same stage — and // inside a stage's subshell or brace group belongs to that same stage — and
// overwritten only when a nested pipeline starts its own. // overwritten only when a nested pipeline starts its own.
pub(crate) nash_pipeline: Option<crate::gate::PipelineSlot>, pub(crate) hydrashell_pipeline: Option<crate::gate::PipelineSlot>,
} }
impl ExecutionParameters { impl ExecutionParameters {
@@ -494,11 +494,11 @@ async fn spawn_pipeline_processes(
let mut spawn_results = VecDeque::new(); let mut spawn_results = VecDeque::new();
let mut process_group_id: Option<i32> = None; let mut process_group_id: Option<i32> = None;
// nash: when observation is active, one id per multi-stage pipeline correlates // hydrashell: when observation is active, one id per multi-stage pipeline correlates
// both its tapped links and the exec events of the stages either side of them // both its tapped links and the exec events of the stages either side of them
// (docs/NASH.md §5.3). `None` on the unobserved fast path and for a lone // (docs/HYDRASHELL.md §5.3). `None` on the unobserved fast path and for a lone
// command, where there is no link to tap and nothing to correlate. // command, where there is no link to tap and nothing to correlate.
let nash_pipeline_id = if pipeline_len > 1 && crate::gate::is_active() { let hydrashell_pipeline_id = if pipeline_len > 1 && crate::gate::is_active() {
Some(crate::gate::next_pipeline_id()) Some(crate::gate::next_pipeline_id())
} else { } else {
None None
@@ -508,9 +508,9 @@ async fn spawn_pipeline_processes(
// command. // command.
if pipeline_len > 1 { if pipeline_len > 1 {
for link in 0..(pipeline_len - 1) { for link in 0..(pipeline_len - 1) {
if let Some(pipeline_id) = nash_pipeline_id { if let Some(pipeline_id) = hydrashell_pipeline_id {
// nash: interpose a tee on the link so the bytes flowing `a | b` are // hydrashell: interpose a tee on the link so the bytes flowing `a | b` are
// captured (docs/NASH.md §5.3). // captured (docs/HYDRASHELL.md §5.3).
// Producer writes `a_write`; a copier moves bytes to `b_write`; // Producer writes `a_write`; a copier moves bytes to `b_write`;
// consumer reads `b_read`. Slots stay identical to the untapped // consumer reads `b_read`. Slots stay identical to the untapped
// case, so the pop-ordering below is unchanged. // case, so the pop-ordering below is unchanged.
@@ -521,13 +521,13 @@ async fn spawn_pipeline_processes(
// Name both ends from their AST *here*, where the stage is unambiguous. Deriving // Name both ends from their AST *here*, where the stage is unambiguous. Deriving
// the name later from exec events cannot distinguish which command inside a // the name later from exec events cannot distinguish which command inside a
// compound stage owned the pipe, and cannot name either end until it exits. // compound stage owned the pipe, and cannot name either end until it exits.
nash_spawn_pipe_tee( hydrashell_spawn_pipe_tee(
a_read, a_read,
b_write, b_write,
pipeline_id, pipeline_id,
from_index, from_index,
nash_stage_text(pipeline, from_index), hydrashell_stage_text(pipeline, from_index),
nash_stage_text(pipeline, from_index + 1), hydrashell_stage_text(pipeline, from_index + 1),
); );
pipe_readers.push(Some(openfiles::OpenFile::PipeReader(b_read))); pipe_readers.push(Some(openfiles::OpenFile::PipeReader(b_read)));
pipe_writers.push(Some(openfiles::OpenFile::PipeWriter(a_write))); pipe_writers.push(Some(openfiles::OpenFile::PipeWriter(a_write)));
@@ -559,11 +559,11 @@ async fn spawn_pipeline_processes(
// Set up parameters appropriate for this command. // Set up parameters appropriate for this command.
let mut cmd_params = params.clone(); let mut cmd_params = params.clone();
// nash: stamp this stage's slot so its exec event can be joined to the links // hydrashell: stamp this stage's slot so its exec event can be joined to the links
// either side of it. Only when this pipeline is itself tapped — a lone command // either side of it. Only when this pipeline is itself tapped — a lone command
// must keep any slot inherited from the stage that contains it. // must keep any slot inherited from the stage that contains it.
if let Some(id) = nash_pipeline_id { if let Some(id) = hydrashell_pipeline_id {
cmd_params.nash_pipeline = Some(crate::gate::PipelineSlot { cmd_params.hydrashell_pipeline = Some(crate::gate::PipelineSlot {
id, id,
index: current_pipeline_index, index: current_pipeline_index,
len: pipeline_len, len: pipeline_len,
@@ -1728,11 +1728,11 @@ pub(crate) async fn setup_redirect(
} }
let expanded_file_path = expanded_fields.remove(0); let expanded_file_path = expanded_fields.remove(0);
// nash: `&>` / `&>>` writes stdout+stderr to one file — record fd 1 // hydrashell: `&>` / `&>>` writes stdout+stderr to one file — record fd 1
// for read-back (docs/NASH.md §5.2). // for read-back (docs/HYDRASHELL.md §5.2).
let abs = shell.absolute_path(Path::new(expanded_file_path.as_str())); let abs = shell.absolute_path(Path::new(expanded_file_path.as_str()));
let size_before = std::fs::metadata(&abs).map(|m| m.len()).unwrap_or(0); let size_before = std::fs::metadata(&abs).map(|m| m.len()).unwrap_or(0);
params.nash_redirects.push(crate::gate::RedirectRecord { params.hydrashell_redirects.push(crate::gate::RedirectRecord {
op: if *append { "&>>".to_string() } else { "&>".to_string() }, op: if *append { "&>>".to_string() } else { "&>".to_string() },
fd: 1, fd: 1,
path: Some(abs), path: Some(abs),
@@ -1820,9 +1820,9 @@ pub(crate) async fn setup_redirect(
) )
})?; })?;
// nash: record the redirect so the observer can read back the // hydrashell: record the redirect so the observer can read back the
// affected byte range after the command runs (docs/NASH.md §5.2). // affected byte range after the command runs (docs/HYDRASHELL.md §5.2).
nash_record_file_redirect(params, fd_num, kind, &expanded_file_path); hydrashell_record_file_redirect(params, fd_num, kind, &expanded_file_path);
params.open_files.set_fd(fd_num, opened_file); params.open_files.set_fd(fd_num, opened_file);
} }
@@ -1946,8 +1946,8 @@ pub(crate) async fn setup_redirect(
let f = setup_open_file_with_contents(io_here_doc.as_str())?; let f = setup_open_file_with_contents(io_here_doc.as_str())?;
// nash: heredoc body is known at setup — capture inline (docs/NASH.md §5.2b). // hydrashell: heredoc body is known at setup — capture inline (docs/HYDRASHELL.md §5.2b).
nash_record_inline_redirect(params, fd_num, "<<", &io_here_doc); hydrashell_record_inline_redirect(params, fd_num, "<<", &io_here_doc);
params.open_files.set_fd(fd_num, f); params.open_files.set_fd(fd_num, f);
} }
@@ -1961,8 +1961,8 @@ pub(crate) async fn setup_redirect(
let f = setup_open_file_with_contents(expanded_word.as_str())?; let f = setup_open_file_with_contents(expanded_word.as_str())?;
// nash: here-string body is known at setup — capture inline. // hydrashell: here-string body is known at setup — capture inline.
nash_record_inline_redirect(params, fd_num, "<<<", &expanded_word); hydrashell_record_inline_redirect(params, fd_num, "<<<", &expanded_word);
params.open_files.set_fd(fd_num, f); params.open_files.set_fd(fd_num, f);
} }
@@ -1971,64 +1971,64 @@ pub(crate) async fn setup_redirect(
Ok(()) Ok(())
} }
// nash: bounded prefix captured per pipe link before reporting (docs/NASH.md §5.3). // hydrashell: bounded prefix captured per pipe link before reporting (docs/HYDRASHELL.md §5.3).
const NASH_PIPE_CAPTURE_CAP: usize = 64 * 1024; const HYDRASHELL_PIPE_CAPTURE_CAP: usize = 64 * 1024;
// nash: longest stage label carried on a pipe event. A stage can be an entire `while` loop, and // hydrashell: longest stage label carried on a pipe event. A stage can be an entire `while` loop, and
// this rides on every link of every pipeline — one of the highest-volume kinds nash emits. // this rides on every link of every pipeline — one of the highest-volume kinds hydrashell emits.
const NASH_STAGE_TEXT_CAP: usize = 200; const HYDRASHELL_STAGE_TEXT_CAP: usize = 200;
// nash: the pipeline stage at `index`, rendered back to shell syntax to label a pipe link // hydrashell: the pipeline stage at `index`, rendered back to shell syntax to label a pipe link
// (docs/NASH.md §5.3). // (docs/HYDRASHELL.md §5.3).
// //
// Collapsing and capping happen here rather than in the observer because this is a *label*, and // Collapsing and capping happen here rather than in the observer because this is a *label*, and
// only brush-core has the AST to render one; redaction still happens observer-side with every // only brush-core has the AST to render one; redaction still happens observer-side with every
// other outbound string. `Display` re-renders the parsed command, so the result is the stage as // other outbound string. `Display` re-renders the parsed command, so the result is the stage as
// parsed rather than byte-identical source — which is what a one-line label wants anyway. // parsed rather than byte-identical source — which is what a one-line label wants anyway.
fn nash_stage_text(pipeline: &ast::Pipeline, index: usize) -> String { fn hydrashell_stage_text(pipeline: &ast::Pipeline, index: usize) -> String {
let Some(command) = pipeline.seq.get(index) else { let Some(command) = pipeline.seq.get(index) else {
return String::new(); return String::new();
}; };
// A compound stage renders multi-line; a label has one line. // A compound stage renders multi-line; a label has one line.
let collapsed = command.to_string(); let collapsed = command.to_string();
let mut text = collapsed.split_whitespace().collect::<Vec<_>>().join(" "); let mut text = collapsed.split_whitespace().collect::<Vec<_>>().join(" ");
if text.chars().count() > NASH_STAGE_TEXT_CAP { if text.chars().count() > HYDRASHELL_STAGE_TEXT_CAP {
text = text.chars().take(NASH_STAGE_TEXT_CAP).collect::<String>(); text = text.chars().take(HYDRASHELL_STAGE_TEXT_CAP).collect::<String>();
text.push('…'); text.push('…');
} }
text text
} }
// nash: bytes moved per userspace copy on the portable tee path. 128 KiB rather than the 32 KiB // hydrashell: bytes moved per userspace copy on the portable tee path. 128 KiB rather than the 32 KiB
// this started with: the same stream costs a quarter of the read/write pairs, and a pipe link's // this started with: the same stream costs a quarter of the read/write pairs, and a pipe link's
// tee cost is almost entirely syscalls and the context switches around them // tee cost is almost entirely syscalls and the context switches around them
// (docs/NASH_STREAM_PERF_PLAN.md §P2.2). // (docs/HYDRASHELL_STREAM_PERF_PLAN.md §P2.2).
const NASH_TEE_BUF: usize = 128 * 1024; const HYDRASHELL_TEE_BUF: usize = 128 * 1024;
// nash: how large both ends of a tapped link are grown to, where the platform allows it. A tee // hydrashell: how large both ends of a tapped link are grown to, where the platform allows it. A tee
// puts *two* pipes where the shell asked for one, so a producer that outruns the default 64 KiB // puts *two* pipes where the shell asked for one, so a producer that outruns the default 64 KiB
// buffer pays for it twice; growing them cuts the wakeups on both sides. // buffer pays for it twice; growing them cuts the wakeups on both sides.
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
const NASH_TEE_PIPE_SIZE: libc::c_int = 256 * 1024; const HYDRASHELL_TEE_PIPE_SIZE: libc::c_int = 256 * 1024;
// nash: bytes asked of one `splice` call. The kernel moves at most a pipe-buffer's worth per call // hydrashell: bytes asked of one `splice` call. The kernel moves at most a pipe-buffer's worth per call
// regardless, so this only has to be comfortably larger than the pipe. // regardless, so this only has to be comfortably larger than the pipe.
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
const NASH_TEE_SPLICE_CHUNK: usize = 1024 * 1024; const HYDRASHELL_TEE_SPLICE_CHUNK: usize = 1024 * 1024;
// nash: how many finished tee threads stay parked waiting for the next pipeline. Thread creation // hydrashell: how many finished tee threads stay parked waiting for the next pipeline. Thread creation
// is the bulk of the ~0.13 ms a tapped pipeline costs to *set up*, and shell-heavy builds run // is the bulk of the ~0.13 ms a tapped pipeline costs to *set up*, and shell-heavy builds run
// pipelines in the thousands (docs/NASH_STREAM_PERF_PLAN.md §P2.3). A shell runs its pipelines // pipelines in the thousands (docs/HYDRASHELL_STREAM_PERF_PLAN.md §P2.3). A shell runs its pipelines
// mostly one at a time, so a small pool covers the common case; a burst of concurrent pipelines // mostly one at a time, so a small pool covers the common case; a burst of concurrent pipelines
// simply spawns beyond it and lets the extra workers retire when they finish. // simply spawns beyond it and lets the extra workers retire when they finish.
const NASH_TEE_POOL_MAX_IDLE: usize = 4; const HYDRASHELL_TEE_POOL_MAX_IDLE: usize = 4;
// nash: stack for a tee worker. It copies through a heap buffer and hands the gate an already-built // hydrashell: stack for a tee worker. It copies through a heap buffer and hands the gate an already-built
// event, so its own frames are shallow — a quarter of the 2 MiB default is ample and maps less. // event, so its own frames are shallow — a quarter of the 2 MiB default is ample and maps less.
const NASH_TEE_STACK: usize = 512 * 1024; const HYDRASHELL_TEE_STACK: usize = 512 * 1024;
// nash: one link waiting to be copied — what a tee worker is handed. // hydrashell: one link waiting to be copied — what a tee worker is handed.
struct NashTeeJob { struct HydrashellTeeJob {
src: std::io::PipeReader, src: std::io::PipeReader,
dst: std::io::PipeWriter, dst: std::io::PipeWriter,
pipeline_id: u64, pipeline_id: u64,
@@ -2037,62 +2037,62 @@ struct NashTeeJob {
to_text: String, to_text: String,
} }
// nash: senders of the tee workers currently parked, newest last. // hydrashell: senders of the tee workers currently parked, newest last.
static NASH_TEE_POOL: std::sync::Mutex<Vec<std::sync::mpsc::Sender<NashTeeJob>>> = static HYDRASHELL_TEE_POOL: std::sync::Mutex<Vec<std::sync::mpsc::Sender<HydrashellTeeJob>>> =
std::sync::Mutex::new(Vec::new()); std::sync::Mutex::new(Vec::new());
// nash: the process the parked workers belong to. A `fork` copies the pool's *memory* but none of // hydrashell: the process the parked workers belong to. A `fork` copies the pool's *memory* but none of
// its threads, so a child that inherited it would hand jobs to workers that do not exist — and a // its threads, so a child that inherited it would hand jobs to workers that do not exist — and a
// dropped job silently breaks the pipeline it was tapping. Checked (and reset) on every dispatch. // dropped job silently breaks the pipeline it was tapping. Checked (and reset) on every dispatch.
static NASH_TEE_POOL_PID: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0); static HYDRASHELL_TEE_POOL_PID: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
// nash: take a parked worker, if this process has one to spare. // hydrashell: take a parked worker, if this process has one to spare.
// //
// `try_lock`, never `lock`: this runs on the shell's own pipeline-setup path, so waiting on the // `try_lock`, never `lock`: this runs on the shell's own pipeline-setup path, so waiting on the
// pool would put observation in front of a command — and after a `fork` the mutex can be left // pool would put observation in front of a command — and after a `fork` the mutex can be left
// locked by a thread that no longer exists. Failing to take a worker just means spawning one. // locked by a thread that no longer exists. Failing to take a worker just means spawning one.
fn nash_tee_take_idle() -> Option<std::sync::mpsc::Sender<NashTeeJob>> { fn hydrashell_tee_take_idle() -> Option<std::sync::mpsc::Sender<HydrashellTeeJob>> {
let mut pool = NASH_TEE_POOL.try_lock().ok()?; let mut pool = HYDRASHELL_TEE_POOL.try_lock().ok()?;
let pid = std::process::id(); let pid = std::process::id();
if NASH_TEE_POOL_PID.swap(pid, std::sync::atomic::Ordering::Relaxed) != pid { if HYDRASHELL_TEE_POOL_PID.swap(pid, std::sync::atomic::Ordering::Relaxed) != pid {
pool.clear(); // inherited across a fork: those workers are not in this process pool.clear(); // inherited across a fork: those workers are not in this process
} }
pool.pop() pool.pop()
} }
// nash: park a worker that just finished a link. Returns whether it was taken — a worker the pool // hydrashell: park a worker that just finished a link. Returns whether it was taken — a worker the pool
// has no room for (or cannot reach) simply exits. // has no room for (or cannot reach) simply exits.
fn nash_tee_park(tx: &std::sync::mpsc::Sender<NashTeeJob>) -> bool { fn hydrashell_tee_park(tx: &std::sync::mpsc::Sender<HydrashellTeeJob>) -> bool {
let Ok(mut pool) = NASH_TEE_POOL.try_lock() else { let Ok(mut pool) = HYDRASHELL_TEE_POOL.try_lock() else {
return false; return false;
}; };
if pool.len() >= NASH_TEE_POOL_MAX_IDLE { if pool.len() >= HYDRASHELL_TEE_POOL_MAX_IDLE {
return false; return false;
} }
pool.push(tx.clone()); pool.push(tx.clone());
true true
} }
// nash: a parked worker's loop — copy a link, park again, wait for the next one. // hydrashell: a parked worker's loop — copy a link, park again, wait for the next one.
fn nash_tee_worker( fn hydrashell_tee_worker(
rx: &std::sync::mpsc::Receiver<NashTeeJob>, rx: &std::sync::mpsc::Receiver<HydrashellTeeJob>,
parked: &std::sync::mpsc::Sender<NashTeeJob>, parked: &std::sync::mpsc::Sender<HydrashellTeeJob>,
) { ) {
while let Ok(job) = rx.recv() { while let Ok(job) = rx.recv() {
nash_run_pipe_tee(job); hydrashell_run_pipe_tee(job);
if !nash_tee_park(parked) { if !hydrashell_tee_park(parked) {
return; return;
} }
} }
} }
// nash: hand a copier the link `src` (producer's output) → `dst` (consumer's input): it mirrors a // hydrashell: hand a copier the link `src` (producer's output) → `dst` (consumer's input): it mirrors a
// bounded prefix, then reports the link to the gate (docs/NASH.md §5.3). The synchronous copy // bounded prefix, then reports the link to the gate (docs/HYDRASHELL.md §5.3). The synchronous copy
// preserves pipe backpressure; EOF on `src` or EPIPE on `dst` (consumer gone, e.g. `yes | head`) // preserves pipe backpressure; EOF on `src` or EPIPE on `dst` (consumer gone, e.g. `yes | head`)
// ends it and closes `dst` so the consumer sees EOF. // ends it and closes `dst` so the consumer sees EOF.
// `to_index` is always `from_index + 1` — a link joins adjacent stages — so it is derived rather // `to_index` is always `from_index + 1` — a link joins adjacent stages — so it is derived rather
// than passed; `from_text`/`to_text` are the two stages already rendered by `nash_stage_text`. // than passed; `from_text`/`to_text` are the two stages already rendered by `hydrashell_stage_text`.
fn nash_spawn_pipe_tee( fn hydrashell_spawn_pipe_tee(
src: std::io::PipeReader, src: std::io::PipeReader,
dst: std::io::PipeWriter, dst: std::io::PipeWriter,
pipeline_id: u64, pipeline_id: u64,
@@ -2100,7 +2100,7 @@ fn nash_spawn_pipe_tee(
from_text: String, from_text: String,
to_text: String, to_text: String,
) { ) {
let mut job = NashTeeJob { let mut job = HydrashellTeeJob {
src, src,
dst, dst,
pipeline_id, pipeline_id,
@@ -2110,30 +2110,30 @@ fn nash_spawn_pipe_tee(
}; };
// A worker that retired between being parked and being handed this job returns it, so try the // A worker that retired between being parked and being handed this job returns it, so try the
// next one down rather than losing the link. // next one down rather than losing the link.
while let Some(tx) = nash_tee_take_idle() { while let Some(tx) = hydrashell_tee_take_idle() {
match tx.send(job) { match tx.send(job) {
Ok(()) => return, Ok(()) => return,
Err(std::sync::mpsc::SendError(returned)) => job = returned, Err(std::sync::mpsc::SendError(returned)) => job = returned,
} }
} }
let (tx, rx) = std::sync::mpsc::channel::<NashTeeJob>(); let (tx, rx) = std::sync::mpsc::channel::<HydrashellTeeJob>();
let parked = tx.clone(); let parked = tx.clone();
// If the spawn fails there is nobody to copy this link and the job is dropped, closing both // If the spawn fails there is nobody to copy this link and the job is dropped, closing both
// ends — the same outcome an unspawnable tee thread has always had, in a shell that is out of // ends — the same outcome an unspawnable tee thread has always had, in a shell that is out of
// threads either way. // threads either way.
if std::thread::Builder::new() if std::thread::Builder::new()
.name("nash-pipe-tee".into()) .name("hydrashell-pipe-tee".into())
.stack_size(NASH_TEE_STACK) .stack_size(HYDRASHELL_TEE_STACK)
.spawn(move || nash_tee_worker(&rx, &parked)) .spawn(move || hydrashell_tee_worker(&rx, &parked))
.is_ok() .is_ok()
{ {
let _ = tx.send(job); let _ = tx.send(job);
} }
} }
// nash: copy one link through to its consumer, then report it. // hydrashell: copy one link through to its consumer, then report it.
fn nash_run_pipe_tee(job: NashTeeJob) { fn hydrashell_run_pipe_tee(job: HydrashellTeeJob) {
let NashTeeJob { let HydrashellTeeJob {
mut src, mut src,
mut dst, mut dst,
pipeline_id, pipeline_id,
@@ -2141,15 +2141,15 @@ fn nash_run_pipe_tee(job: NashTeeJob) {
from_text, from_text,
to_text, to_text,
} = job; } = job;
nash_grow_pipe(&src); hydrashell_grow_pipe(&src);
nash_grow_pipe(&dst); hydrashell_grow_pipe(&dst);
let mut captured: Vec<u8> = Vec::new(); let mut captured: Vec<u8> = Vec::new();
let mut total: u64 = 0; let mut total: u64 = 0;
// Only the prefix is ever copied through userspace; the rest of the stream — which is all of // Only the prefix is ever copied through userspace; the rest of the stream — which is all of
// it, for the transfers that actually cost something — is handed to the kernel. // it, for the transfers that actually cost something — is handed to the kernel.
if nash_tee_prefix(&mut src, &mut dst, &mut captured, &mut total) { if hydrashell_tee_prefix(&mut src, &mut dst, &mut captured, &mut total) {
nash_tee_passthrough(&mut src, &mut dst, &mut total); hydrashell_tee_passthrough(&mut src, &mut dst, &mut total);
} }
// Report BEFORE closing `dst`: dropping it unblocks the consumer, which // Report BEFORE closing `dst`: dropping it unblocks the consumer, which
@@ -2169,17 +2169,17 @@ fn nash_run_pipe_tee(job: NashTeeJob) {
drop(dst); drop(dst);
} }
// nash: copy until the capture cap is reached, mirroring what goes past. Returns whether the link // hydrashell: copy until the capture cap is reached, mirroring what goes past. Returns whether the link
// is still open (false on EOF, or once either end gives up). // is still open (false on EOF, or once either end gives up).
fn nash_tee_prefix( fn hydrashell_tee_prefix(
src: &mut std::io::PipeReader, src: &mut std::io::PipeReader,
dst: &mut std::io::PipeWriter, dst: &mut std::io::PipeWriter,
captured: &mut Vec<u8>, captured: &mut Vec<u8>,
total: &mut u64, total: &mut u64,
) -> bool { ) -> bool {
use std::io::{Read, Write}; use std::io::{Read, Write};
let mut buf = vec![0u8; NASH_TEE_BUF]; let mut buf = vec![0u8; HYDRASHELL_TEE_BUF];
while captured.len() < NASH_PIPE_CAPTURE_CAP { while captured.len() < HYDRASHELL_PIPE_CAPTURE_CAP {
let n = match src.read(&mut buf) { let n = match src.read(&mut buf) {
Ok(0) => return false, // producer closed → done Ok(0) => return false, // producer closed → done
Ok(n) => n, Ok(n) => n,
@@ -2187,7 +2187,7 @@ fn nash_tee_prefix(
Err(_) => return false, Err(_) => return false,
}; };
*total += n as u64; *total += n as u64;
let room = NASH_PIPE_CAPTURE_CAP - captured.len(); let room = HYDRASHELL_PIPE_CAPTURE_CAP - captured.len();
captured.extend_from_slice(&buf[..room.min(n)]); captured.extend_from_slice(&buf[..room.min(n)]);
// Pass through; if the consumer went away, stop (its EOF is enough). // Pass through; if the consumer went away, stop (its EOF is enough).
if dst.write_all(&buf[..n]).is_err() { if dst.write_all(&buf[..n]).is_err() {
@@ -2197,11 +2197,11 @@ fn nash_tee_prefix(
true true
} }
// nash: the portable passthrough — the same read/write loop as the prefix phase, minus the // hydrashell: the portable passthrough — the same read/write loop as the prefix phase, minus the
// capture. Used on platforms without `splice`, and as the fallback when the kernel refuses it. // capture. Used on platforms without `splice`, and as the fallback when the kernel refuses it.
fn nash_tee_copy(src: &mut std::io::PipeReader, dst: &mut std::io::PipeWriter, total: &mut u64) { fn hydrashell_tee_copy(src: &mut std::io::PipeReader, dst: &mut std::io::PipeWriter, total: &mut u64) {
use std::io::{Read, Write}; use std::io::{Read, Write};
let mut buf = vec![0u8; NASH_TEE_BUF]; let mut buf = vec![0u8; HYDRASHELL_TEE_BUF];
loop { loop {
let n = match src.read(&mut buf) { let n = match src.read(&mut buf) {
Ok(0) => return, Ok(0) => return,
@@ -2216,12 +2216,12 @@ fn nash_tee_copy(src: &mut std::io::PipeReader, dst: &mut std::io::PipeWriter, t
} }
} }
// nash: move the rest of the link pipe-to-pipe inside the kernel (docs/NASH_STREAM_PERF_PLAN.md // hydrashell: move the rest of the link pipe-to-pipe inside the kernel (docs/HYDRASHELL_STREAM_PERF_PLAN.md
// §P2.1). Past the captured prefix the tee has nothing to look at, so there is no reason for the // §P2.1). Past the captured prefix the tee has nothing to look at, so there is no reason for the
// bytes to enter this process at all — and the byte count comes back from the same call that // bytes to enter this process at all — and the byte count comes back from the same call that
// moves them. // moves them.
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn nash_tee_passthrough( fn hydrashell_tee_passthrough(
src: &mut std::io::PipeReader, src: &mut std::io::PipeReader,
dst: &mut std::io::PipeWriter, dst: &mut std::io::PipeWriter,
total: &mut u64, total: &mut u64,
@@ -2237,7 +2237,7 @@ fn nash_tee_passthrough(
std::ptr::null_mut(), std::ptr::null_mut(),
out_fd, out_fd,
std::ptr::null_mut(), std::ptr::null_mut(),
NASH_TEE_SPLICE_CHUNK, HYDRASHELL_TEE_SPLICE_CHUNK,
libc::SPLICE_F_MOVE, libc::SPLICE_F_MOVE,
) )
}; };
@@ -2254,7 +2254,7 @@ fn nash_tee_passthrough(
// A kernel (or a seccomp policy) that will not splice these descriptors: finish the // A kernel (or a seccomp policy) that will not splice these descriptors: finish the
// link the portable way rather than dropping it. // link the portable way rather than dropping it.
Some(libc::EINVAL | libc::ENOSYS | libc::EPERM) => { Some(libc::EINVAL | libc::ENOSYS | libc::EPERM) => {
nash_tee_copy(src, dst, total); hydrashell_tee_copy(src, dst, total);
return; return;
} }
// Consumer gone (EPIPE) or a link that broke: its EOF is enough. // Consumer gone (EPIPE) or a link that broke: its EOF is enough.
@@ -2264,29 +2264,29 @@ fn nash_tee_passthrough(
} }
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
fn nash_tee_passthrough( fn hydrashell_tee_passthrough(
src: &mut std::io::PipeReader, src: &mut std::io::PipeReader,
dst: &mut std::io::PipeWriter, dst: &mut std::io::PipeWriter,
total: &mut u64, total: &mut u64,
) { ) {
nash_tee_copy(src, dst, total); hydrashell_tee_copy(src, dst, total);
} }
// nash: grow a tapped pipe's buffer, best-effort. The kernel caps unprivileged growth at // hydrashell: grow a tapped pipe's buffer, best-effort. The kernel caps unprivileged growth at
// `/proc/sys/fs/pipe-max-size` and simply refuses anything larger, which costs one failed syscall // `/proc/sys/fs/pipe-max-size` and simply refuses anything larger, which costs one failed syscall
// per link and leaves the default in place. // per link and leaves the default in place.
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn nash_grow_pipe<F: std::os::fd::AsRawFd>(pipe: &F) { fn hydrashell_grow_pipe<F: std::os::fd::AsRawFd>(pipe: &F) {
// SAFETY: `fcntl` with `F_SETPIPE_SZ` takes an int and only reads the descriptor's pipe size. // SAFETY: `fcntl` with `F_SETPIPE_SZ` takes an int and only reads the descriptor's pipe size.
let _ = unsafe { libc::fcntl(pipe.as_raw_fd(), libc::F_SETPIPE_SZ, NASH_TEE_PIPE_SIZE) }; let _ = unsafe { libc::fcntl(pipe.as_raw_fd(), libc::F_SETPIPE_SZ, HYDRASHELL_TEE_PIPE_SIZE) };
} }
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
fn nash_grow_pipe<F>(_pipe: &F) {} fn hydrashell_grow_pipe<F>(_pipe: &F) {}
// nash: record a regular-file redirect for post-run read-back (docs/NASH.md §5.2a). // hydrashell: record a regular-file redirect for post-run read-back (docs/HYDRASHELL.md §5.2a).
// The child still gets the real fd; this only notes what to read back afterward. // The child still gets the real fd; this only notes what to read back afterward.
fn nash_record_file_redirect( fn hydrashell_record_file_redirect(
params: &mut ExecutionParameters, params: &mut ExecutionParameters,
fd: ShellFd, fd: ShellFd,
kind: &ast::IoFileRedirectKind, kind: &ast::IoFileRedirectKind,
@@ -2303,7 +2303,7 @@ fn nash_record_file_redirect(
// Size at setup: append ranges start here; only regular files are read back. // Size at setup: append ranges start here; only regular files are read back.
let size_before = std::fs::metadata(path).map(|m| m.len()).unwrap_or(0); let size_before = std::fs::metadata(path).map(|m| m.len()).unwrap_or(0);
let op = if fd == 2 && op == ">" { "2>".to_string() } else { op.to_string() }; let op = if fd == 2 && op == ">" { "2>".to_string() } else { op.to_string() };
params.nash_redirects.push(crate::gate::RedirectRecord { params.hydrashell_redirects.push(crate::gate::RedirectRecord {
op, op,
fd: u32::try_from(fd).unwrap_or(0), fd: u32::try_from(fd).unwrap_or(0),
path: Some(path.to_path_buf()), path: Some(path.to_path_buf()),
@@ -2313,14 +2313,14 @@ fn nash_record_file_redirect(
}); });
} }
// nash: record an inline (heredoc / here-string) redirect — body known at setup. // hydrashell: record an inline (heredoc / here-string) redirect — body known at setup.
fn nash_record_inline_redirect( fn hydrashell_record_inline_redirect(
params: &mut ExecutionParameters, params: &mut ExecutionParameters,
fd: ShellFd, fd: ShellFd,
op: &str, op: &str,
body: &str, body: &str,
) { ) {
params.nash_redirects.push(crate::gate::RedirectRecord { params.hydrashell_redirects.push(crate::gate::RedirectRecord {
op: op.to_string(), op: op.to_string(),
fd: u32::try_from(fd).unwrap_or(0), fd: u32::try_from(fd).unwrap_or(0),
path: None, path: None,
+1 -1
View File
@@ -14,7 +14,7 @@ pub mod expansion;
mod extendedtests; mod extendedtests;
pub mod extensions; pub mod extensions;
pub mod functions; pub mod functions;
// nash: observation/enforcement gate (docs/NASH.md §4.2). // hydrashell: observation/enforcement gate (docs/HYDRASHELL.md §4.2).
pub mod gate; pub mod gate;
pub mod history; pub mod history;
pub mod int_utils; pub mod int_utils;
+4 -4
View File
@@ -17,7 +17,7 @@ pub struct ChildProcess {
pid: Option<sys::process::ProcessId>, pid: Option<sys::process::ProcessId>,
/// If available, the process group ID of the child. /// If available, the process group ID of the child.
pgid: Option<sys::process::ProcessId>, pgid: Option<sys::process::ProcessId>,
// nash: gate token issued by `Gate::on_exec` for this command, reported // hydrashell: gate token issued by `Gate::on_exec` for this command, reported
// back via `Gate::on_exit` when the process completes. // back via `Gate::on_exit` when the process completes.
pub(crate) gate_token: Option<u64>, pub(crate) gate_token: Option<u64>,
} }
@@ -59,7 +59,7 @@ impl ChildProcess {
tokio::select! { tokio::select! {
output = &mut self.exec_future => { output = &mut self.exec_future => {
let output = output?; let output = output?;
// nash: report process completion to the gate exactly once. // hydrashell: report process completion to the gate exactly once.
if let Some(token) = self.gate_token.take() { if let Some(token) = self.gate_token.take() {
crate::gate::gate().on_exit( crate::gate::gate().on_exit(
token, token,
@@ -90,7 +90,7 @@ impl ChildProcess {
let result: Option<Result<std::process::Output, error::Error>> = checkable_future let result: Option<Result<std::process::Output, error::Error>> = checkable_future
.now_or_never() .now_or_never()
.map(|result| result.map_err(Into::into)); .map(|result| result.map_err(Into::into));
// nash: completion can also be observed via poll (e.g. job checks). // hydrashell: completion can also be observed via poll (e.g. job checks).
if let Some(Ok(output)) = &result { if let Some(Ok(output)) = &result {
if let Some(token) = self.gate_token.take() { if let Some(token) = self.gate_token.take() {
crate::gate::gate().on_exit( crate::gate::gate().on_exit(
@@ -105,7 +105,7 @@ impl ChildProcess {
} }
} }
// nash: numeric exit code for gate reporting (128+signal for signal deaths). // hydrashell: numeric exit code for gate reporting (128+signal for signal deaths).
fn exit_code_of(status: &std::process::ExitStatus) -> i32 { fn exit_code_of(status: &std::process::ExitStatus) -> i32 {
if let Some(code) = status.code() { if let Some(code) = status.code() {
return code; return code;
+1 -1
View File
@@ -87,7 +87,7 @@ fn format_prompt_piece(
if users::is_root() { if users::is_root() {
"#".to_owned() "#".to_owned()
} else { } else {
// nash: the interactive shell symbol is the atom sign (U+269B) rather than `$`. // hydrashell: the interactive shell symbol is the atom sign (U+269B) rather than `$`.
"\u{269B}".to_owned() "\u{269B}".to_owned()
} }
} }
+3 -3
View File
@@ -14,14 +14,14 @@ brush is distributed under the terms of the MIT license. If you encounter any is
For more information, visit https://brush.sh."; For more information, visit https://brush.sh.";
// nash: rebranded usage/version strings for the Nucleic agent shell fork. // hydrashell: rebranded usage/version strings for the Nucleic agent shell fork.
const USAGE: &str = color_print::cstr!( const USAGE: &str = color_print::cstr!(
"<bold>nash</bold> <italics>[OPTIONS]</italics>... <italics>[SCRIPT_PATH [SCRIPT_ARGS]...]</italics>" "<bold>hydrashell</bold> <italics>[OPTIONS]</italics>... <italics>[SCRIPT_PATH [SCRIPT_ARGS]...]</italics>"
); );
const VERSION: &str = const_format::concatcp!( const VERSION: &str = const_format::concatcp!(
productinfo::PRODUCT_VERSION, productinfo::PRODUCT_VERSION,
" (Nucleic agent shell, brush fork; ", " (Hydrangea agent shell, brush fork; ",
productinfo::PRODUCT_GIT_VERSION, productinfo::PRODUCT_GIT_VERSION,
")" ")"
); );
+3 -3
View File
@@ -268,11 +268,11 @@ pub fn run() {
std::process::exit(i32::from(exit_code)); std::process::exit(i32::from(exit_code));
} }
// nash: a hook invoked right before the process exits, so an embedding shell // hydrashell: a hook invoked right before the process exits, so an embedding shell
// (nash) can flush its observation event buffer (docs/NASH.md §6.1). // (hash) can flush its observation event buffer (docs/HYDRASHELL.md §6.1).
static EXIT_HOOK: std::sync::OnceLock<Box<dyn Fn() + Send + Sync>> = std::sync::OnceLock::new(); static EXIT_HOOK: std::sync::OnceLock<Box<dyn Fn() + Send + Sync>> = std::sync::OnceLock::new();
/// nash: registers a hook run immediately before the shell process exits. /// hydrashell: registers a hook run immediately before the shell process exits.
/// May be called at most once; later calls are ignored. /// May be called at most once; later calls are ignored.
pub fn set_exit_hook(hook: Box<dyn Fn() + Send + Sync>) { pub fn set_exit_hook(hook: Box<dyn Fn() + Send + Sync>) {
let _ = EXIT_HOOK.set(hook); let _ = EXIT_HOOK.set(hook);
+4 -4
View File
@@ -1,8 +1,8 @@
//! Information about this shell project. //! Information about this shell project.
// nash: rebranded from "brush" for the Nucleic agent shell fork (docs/NASH.md §2). // hydrashell: rebranded from "brush" for the Nucleic agent shell fork (docs/HYDRASHELL.md §2).
/// The formal name of this product. /// The formal name of this product.
pub const PRODUCT_NAME: &str = "nash"; pub const PRODUCT_NAME: &str = "hydrashell";
const PRODUCT_HOMEPAGE: &str = env!("CARGO_PKG_HOMEPAGE"); const PRODUCT_HOMEPAGE: &str = env!("CARGO_PKG_HOMEPAGE");
const PRODUCT_REPO: &str = env!("CARGO_PKG_REPOSITORY"); const PRODUCT_REPO: &str = env!("CARGO_PKG_REPOSITORY");
@@ -27,8 +27,8 @@ pub const PRODUCT_GIT_VERSION: &str = git_version::git_version!(
); );
pub(crate) fn get_product_display_str() -> String { pub(crate) fn get_product_display_str() -> String {
// nash: identify as the Nucleic agent shell fork of brush. // hydrashell: identify as the Nucleic agent shell fork of brush.
std::format!( std::format!(
"{PRODUCT_NAME} (Nucleic agent shell, brush fork) {PRODUCT_VERSION} ({PRODUCT_GIT_VERSION}) - {PRODUCT_DISPLAY_URI}" "{PRODUCT_NAME} (hash — Hydrangea agent shell, brush fork) {PRODUCT_VERSION} ({PRODUCT_GIT_VERSION}) - {PRODUCT_DISPLAY_URI}"
) )
} }
+3 -3
View File
@@ -219,7 +219,7 @@ cases:
echo "After: [$IFS]" echo "After: [$IFS]"
- name: "IFS does not affect for loop literal words" - name: "IFS does not affect for loop literal words"
# nash: fixed by ExpansionPiece::LiteralText (literal words are not field-split) # hydrashell: fixed by ExpansionPiece::LiteralText (literal words are not field-split)
stdin: | stdin: |
IFS=':' IFS=':'
for word in a:b:c; do for word in a:b:c; do
@@ -249,7 +249,7 @@ cases:
echo "Quoted: $var" echo "Quoted: $var"
- name: "IFS with partially quoted expansion" - name: "IFS with partially quoted expansion"
# nash: fixed by ExpansionPiece::LiteralText (literal words are not field-split) # hydrashell: fixed by ExpansionPiece::LiteralText (literal words are not field-split)
stdin: | stdin: |
IFS=':' IFS=':'
var="a:b:c" var="a:b:c"
@@ -309,7 +309,7 @@ cases:
echo "[$1]" echo "[$1]"
- name: "IFS with glob pattern literal" - name: "IFS with glob pattern literal"
# nash: fixed by ExpansionPiece::LiteralText (literal words are not field-split) # hydrashell: fixed by ExpansionPiece::LiteralText (literal words are not field-split)
stdin: | stdin: |
IFS=':' IFS=':'
# Glob patterns as literals shouldn't split # Glob patterns as literals shouldn't split