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())) } 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)?;