diff --git a/NUCLEIC_FORK.md b/NUCLEIC_FORK.md index 60bc0b0..a511ec3 100644 --- a/NUCLEIC_FORK.md +++ b/NUCLEIC_FORK.md @@ -18,6 +18,10 @@ Every fork change is marked with a `// nash:` comment. Current patches: | `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-shell/src/entry.rs` | `set_exit_hook` seam so nash 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/interp.rs` | `ExecutionParameters.nash_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/expansion.rs` | command-substitution arm calls `gate().on_cmdsub` with the trimmed output | 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 @@ -26,8 +30,11 @@ pristine `brush-shell-v0.4.0` baseline run (environmental: ANSI-C quote and zero fork-caused regressions; +3 successes vs baseline are the repaired IFS tests. nash corpus: 94/94. -Planned (M2, per docs/NASH.md §5.2–5.3): redirect/pipe tap hooks on the `Gate` -trait (`on_redirect`/`on_pipe`) in `interp.rs`/`openfiles.rs`. +Planned (M2 remainder, per docs/NASH.md §5.3): inline tees on the pipe wiring +in `interp.rs::spawn_pipeline_processes` (the `std::io::pipe()` links between +`a | b` stages) — the one data-flow channel not yet tapped. Deferred as the most +invasive change (async copier + backpressure + SIGPIPE); regular-file +redirections, heredoc/here-string, and command substitution are all captured. Updating: diff against the upstream tag, re-vendor, re-apply `// nash:` patches (grep for the marker), then re-run `shell/corpus/replay.py` and the brush compat diff --git a/brush-core/src/commands.rs b/brush-core/src/commands.rs index 99af699..6eb8e1a 100644 --- a/brush-core/src/commands.rs +++ b/brush-core/src/commands.rs @@ -353,7 +353,7 @@ impl<'a, SE: extensions::ShellExtensions> SimpleCommand<'a, SE> { /// command passes through the process-global [`crate::gate::Gate`] before /// dispatch; completion is reported back with the issued token, however the /// command runs (inline, spawned process, or spawned task). - pub async fn execute(self) -> Result { + pub async fn execute(mut self) -> Result { use std::io::Write; let gate = crate::gate::gate(); @@ -362,9 +362,12 @@ impl<'a, SE: extensions::ShellExtensions> SimpleCommand<'a, SE> { if argv.is_empty() { argv.push(self.command_name.clone()); } + // nash: hand this command's redirects to the observer for post-run read-back. + let redirects = std::mem::take(&mut self.params.nash_redirects); let start_event = crate::gate::ExecStart { argv, cwd: self.shell.working_dir().to_path_buf(), + redirects, }; let token = match gate.on_exec(start_event) { crate::gate::Verdict::Allow(token) => token, diff --git a/brush-core/src/expansion.rs b/brush-core/src/expansion.rs index d6a7385..74a3524 100644 --- a/brush-core/src/expansion.rs +++ b/brush-core/src/expansion.rs @@ -999,6 +999,12 @@ impl<'a, SE: extensions::ShellExtensions> WordExpander<'a, SE> { let trimmed_len = cmd_output.trim_end_matches('\n').len(); cmd_output.truncate(trimmed_len); + // nash: capture command-substitution output (docs/NASH.md §5.3) — + // invisible to the transcript, often carries decisions/secrets. + if !self.disable_command_substitutions { + crate::gate::gate().on_cmdsub(cmd_output.as_str()); + } + Expansion::from(ExpansionPiece::Splittable(cmd_output)) } brush_parser::word::WordPiece::EscapeSequence(s) => { diff --git a/brush-core/src/gate.rs b/brush-core/src/gate.rs index 35dd4af..6794b0a 100644 --- a/brush-core/src/gate.rs +++ b/brush-core/src/gate.rs @@ -18,6 +18,41 @@ pub struct ExecStart { pub argv: Vec, /// The shell's working directory at execution time. pub cwd: PathBuf, + /// nash: the redirections applied to this command (docs/NASH.md §5.2). The + /// observer reads back the affected byte range after the command completes. + pub redirects: Vec, +} + +/// How a redirection's data is captured after the command runs (docs/NASH.md §5.2). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RedirectReadback { + /// A truncating write (`>`, `>|`, `&>`): read the head of the final file. + Truncate, + /// An append (`>>`): read the bytes added after `size_before`. + Append, + /// An input (`<`): read the head of the source file. + Input, + /// Inline data known at setup (heredoc / here-string): no file read-back. + Inline, +} + +/// One redirection observed on a command. For regular files nash hands the child +/// the *real* fd (semantics untouched) and reads back a bounded range afterward; +/// for heredoc/here-string the body is captured inline at setup. +#[derive(Clone, Debug)] +pub struct RedirectRecord { + /// The operator as written (`>`, `>>`, `<`, `2>`, `&>`, `<<`, `<<<`). + pub op: String, + /// The affected file descriptor number. + pub fd: u32, + /// The regular-file target, if any (`None` for heredoc/here-string). + pub path: Option, + /// How to read the data back. + pub readback: RedirectReadback, + /// The file's size at setup time (for append ranges); 0 otherwise. + pub size_before: u64, + /// Inline body for heredoc/here-string (already known at setup). + pub inline: Option, } /// Details about a completed simple command. @@ -49,6 +84,9 @@ pub trait Gate: Send + Sync { /// Called when a simple command completes, with the token issued by /// `on_exec`. fn on_exit(&self, token: u64, ev: ExecEnd); + /// nash: called with the (trimmed) output of a command substitution + /// `$(…)` / backticks (docs/NASH.md §5.3). Default no-op. + fn on_cmdsub(&self, _output: &str) {} } struct AllowAll; diff --git a/brush-core/src/interp.rs b/brush-core/src/interp.rs index 212e305..ce15ff9 100644 --- a/brush-core/src/interp.rs +++ b/brush-core/src/interp.rs @@ -37,6 +37,10 @@ pub struct ExecutionParameters { /// Whether `errexit` (exit on error) behavior should be /// suppressed in this execution context. Defaults to `false`. pub suppress_errexit: bool, + // nash: redirections applied to the command being built, drained into its + // `gate::ExecStart` for post-run read-back (docs/NASH.md §5.2). Empty unless + // observation is active and the command has redirects. + pub(crate) nash_redirects: Vec, } impl ExecutionParameters { @@ -1641,6 +1645,22 @@ pub(crate) async fn setup_redirect( } let expanded_file_path = expanded_fields.remove(0); + // nash: `&>` / `&>>` writes stdout+stderr to one file — record fd 1 + // for read-back (docs/NASH.md §5.2). + 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); + params.nash_redirects.push(crate::gate::RedirectRecord { + op: if *append { "&>>".to_string() } else { "&>".to_string() }, + fd: 1, + path: Some(abs), + readback: if *append { + crate::gate::RedirectReadback::Append + } else { + crate::gate::RedirectReadback::Truncate + }, + size_before, + inline: None, + }); setup_redirect_output_and_error_to(shell, params, &expanded_file_path, *append)?; } @@ -1717,6 +1737,10 @@ pub(crate) async fn setup_redirect( ) })?; + // nash: record the redirect so the observer can read back the + // affected byte range after the command runs (docs/NASH.md §5.2). + nash_record_file_redirect(params, fd_num, kind, &expanded_file_path); + params.open_files.set_fd(fd_num, opened_file); } @@ -1839,6 +1863,9 @@ pub(crate) async fn setup_redirect( 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). + nash_record_inline_redirect(params, fd_num, "<<", &io_here_doc); + params.open_files.set_fd(fd_num, f); } @@ -1851,6 +1878,9 @@ pub(crate) async fn setup_redirect( let f = setup_open_file_with_contents(expanded_word.as_str())?; + // nash: here-string body is known at setup — capture inline. + nash_record_inline_redirect(params, fd_num, "<<<", &expanded_word); + params.open_files.set_fd(fd_num, f); } } @@ -1858,6 +1888,52 @@ pub(crate) async fn setup_redirect( Ok(()) } +// nash: record a regular-file redirect for post-run read-back (docs/NASH.md §5.2a). +// The child still gets the real fd; this only notes what to read back afterward. +fn nash_record_file_redirect( + params: &mut ExecutionParameters, + fd: ShellFd, + kind: &ast::IoFileRedirectKind, + path: &Path, +) { + use crate::gate::RedirectReadback; + let (op, readback) = match kind { + ast::IoFileRedirectKind::Read | ast::IoFileRedirectKind::DuplicateInput => ("<", RedirectReadback::Input), + ast::IoFileRedirectKind::Write | ast::IoFileRedirectKind::DuplicateOutput => (">", RedirectReadback::Truncate), + ast::IoFileRedirectKind::Clobber => (">|", RedirectReadback::Truncate), + ast::IoFileRedirectKind::Append => (">>", RedirectReadback::Append), + ast::IoFileRedirectKind::ReadAndWrite => ("<>", RedirectReadback::Truncate), + }; + // 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 op = if fd == 2 && op == ">" { "2>".to_string() } else { op.to_string() }; + params.nash_redirects.push(crate::gate::RedirectRecord { + op, + fd: u32::try_from(fd).unwrap_or(0), + path: Some(path.to_path_buf()), + readback, + size_before, + inline: None, + }); +} + +// nash: record an inline (heredoc / here-string) redirect — body known at setup. +fn nash_record_inline_redirect( + params: &mut ExecutionParameters, + fd: ShellFd, + op: &str, + body: &str, +) { + params.nash_redirects.push(crate::gate::RedirectRecord { + op: op.to_string(), + fd: u32::try_from(fd).unwrap_or(0), + path: None, + readback: crate::gate::RedirectReadback::Inline, + size_before: 0, + inline: Some(body.to_string()), + }); +} + /// Sets up redirection of both stdout and stderr to the same file, given by `file_path`. /// /// # Arguments