From c0a5041306612f2c535fbccd6e5ad1bc5824f6f1 Mon Sep 17 00:00:00 2001 From: Andrew Cowie Date: Wed, 24 Jun 2026 19:50:24 +1000 Subject: [PATCH 1/2] Compbine stderr with stdout in captured child output --- src/runner/library.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/runner/library.rs b/src/runner/library.rs index 4272f28..3a38e33 100644 --- a/src/runner/library.rs +++ b/src/runner/library.rs @@ -264,12 +264,12 @@ fn pairs(_context: &Context, args: &[Value]) -> Result { Ok(Value::Arraeum(pairs)) } -/// `exec(script)` — run a shell script, teeing its stdout through the Context -/// to the operator as it streams while accumulating it as the return value. -/// Output is held as bytes until the end so a chunk split mid-UTF-8 is -/// harmless and only one String is allocated. Trailing newlines are trimmed -/// from the captured value (matching the usual experience when doing shell -/// `$(...)` substitution). A non-zero exit is an error. +/// `exec(script)` — run a shell script, teeing its combined stdout and stderr +/// through the Context to the operator as it streams while accumulating it as +/// the return value. Output is held as bytes until the end so a chunk split +/// mid-UTF-8 is harmless and only one String is allocated. Trailing newlines +/// are trimmed from the captured value (matching the usual experience when +/// doing shell `$(...)` substitution). A non-zero exit is an error. fn exec(context: &Context, args: &[Value]) -> Result { let script = match &args[0] { Value::Literali(script) => script, @@ -281,9 +281,13 @@ fn exec(context: &Context, args: &[Value]) -> Result { } }; + // Fold the child's stderr into its stdout (`exec 2>&1`) so the two streams + // are teed and captured together as one ordered transcript. A single pipe + // keeps this to one reader, with no risk of deadlocking on a full stderr + // buffer the way two separately drained pipes would. let mut child = Command::new("bash") .arg("-c") - .arg(script) + .arg(format!("exec 2>&1\n{}", script)) .stdout(Stdio::piped()) .spawn() .map_err(RunnerError::ExecError)?; From e7c27730f813f786b3a3788798453027ab12b4f5 Mon Sep 17 00:00:00 2001 From: Andrew Cowie Date: Wed, 24 Jun 2026 19:50:55 +1000 Subject: [PATCH 2/2] Render command being run in prompt --- src/runner/driver.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/runner/driver.rs b/src/runner/driver.rs index a4219de..0aa6700 100644 --- a/src/runner/driver.rs +++ b/src/runner/driver.rs @@ -465,6 +465,16 @@ fn render_settle( let _ = writeln!(out, "{} {}", path, renderer.style(syntax, glyph)); } +/// Render an automatically-run shell command on one line: `{path} $ {script}`, +/// the whole line dark grey like the trace chrome so it reads as announce +/// rather than as the command's own output that follows. The `$` stands in for +/// a shell prompt (distinct from the operator's `▶` edit prompt). Trailing +/// whitespace is trimmed so a code block's blank tail line is not echoed. +fn render_command(out: &mut W, qualified: &str, script: &str, renderer: &dyn Render) { + let line = renderer.style(Syntax::Marker, &format!("{} $ {}", qualified, script.trim_end())); + let _ = writeln!(out, "{}", line); +} + /// Render a Section heading: its numeral and title. fn render_section(out: &mut W, numeral: &str, title: &str, renderer: &dyn Render) { let styled_numeral = renderer.style(crate::formatting::Syntax::StepItem, numeral); @@ -1236,8 +1246,8 @@ impl Driver for Automatic { UserInput::Skip } - fn command(&mut self, _qualified: &str, script: &str) -> UserInput { - write_indented(&mut self.output, script); + fn command(&mut self, qualified: &str, script: &str) -> UserInput { + render_command(&mut self.output, &display_path(qualified), script, self.renderer); UserInput::Done(Value::Literali(script.to_string())) }