Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions src/runner/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,16 @@ fn render_settle<W: Write>(
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<W: Write>(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<W: Write>(out: &mut W, numeral: &str, title: &str, renderer: &dyn Render) {
let styled_numeral = renderer.style(crate::formatting::Syntax::StepItem, numeral);
Expand Down Expand Up @@ -1236,8 +1246,8 @@ impl<W: Write> Driver for Automatic<W> {
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()))
}

Expand Down
18 changes: 11 additions & 7 deletions src/runner/library.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,12 +264,12 @@ fn pairs(_context: &Context, args: &[Value]) -> Result<Value, RunnerError> {
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<Value, RunnerError> {
let script = match &args[0] {
Value::Literali(script) => script,
Expand All @@ -281,9 +281,13 @@ fn exec(context: &Context, args: &[Value]) -> Result<Value, RunnerError> {
}
};

// 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)?;
Expand Down