Merge nucleic/nimble-umber-toad-20h7 into dev
This commit is contained in:
+15
-15
@@ -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`).
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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()?;
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
")"
|
")"
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user