Merge nucleic/sleek-thistle-egret-fyej into dev

This commit is contained in:
2026-07-18 14:31:25 -07:00
parent 26fd468ad0
commit b8ef14999d
5 changed files with 133 additions and 3 deletions
+4 -1
View File
@@ -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<ExecutionSpawnResult, error::Error> {
pub async fn execute(mut self) -> Result<ExecutionSpawnResult, error::Error> {
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,
+6
View File
@@ -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) => {
+38
View File
@@ -18,6 +18,41 @@ pub struct ExecStart {
pub argv: Vec<String>,
/// 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<RedirectRecord>,
}
/// 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<PathBuf>,
/// 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<String>,
}
/// 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;
+76
View File
@@ -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<crate::gate::RedirectRecord>,
}
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