oconnor663 / duct.rs

a Rust library for running child processes
MIT License
795 stars 34 forks source link

getting the command's exit status after using `stderr_to_stdout` #114

Open ezntek opened 9 months ago

ezntek commented 9 months ago

Is it possible for me to call .stderr_to_stdout() on an expression and also get an exit status?

oconnor663 commented 6 months ago

How about this?

fn main() -> anyhow::Result<()> {
    let bash_script = "echo hi && echo lo 1>&2 && exit 42";
    let output = duct::cmd!("bash", "-c", bash_script)
        .stderr_to_stdout()
        .stdout_capture()
        .unchecked()
        .run()?;
    assert_eq!(output.status.code(), Some(42));
    assert_eq!(&output.stdout, b"hi\nlo\n");
    Ok(())
}
ezntek commented 5 months ago

I'm using the lines method to get the stdout and stderr with a BufReader, but trying to access the exit code through the ReaderHandle would be illegal. because the ReaderHandle would have been moved. How do I mitigate this? (get a stream and get the exit code later with try_wait)

oconnor663 commented 5 months ago

Ah this is a bit tricky. The .lines() method on BufReader normally consumes the whole reader, but it's also possible to call .lines() on a &mut BufReader. (There's a blanket impl on BufRead that makes this work.) If you do that, then you can get the original duct::ReaderHandle by calling BufReader::into_inner. The whole operation looks something like this:

use duct::cmd;
use std::io::prelude::*;
use std::io::BufReader;

const PYTHON: &str = r#"
import sys
for i in range(1_000_000):
    sys.stdout.write(f"stdout {i}\n")
    sys.stderr.write(f"stderr {i}\n")
"#;

fn main() -> anyhow::Result<()> {
    let reader = cmd!("python3", "-c", PYTHON).stderr_to_stdout().reader()?;
    let mut buf_reader = BufReader::new(reader);
    let num_lines = (&mut buf_reader).lines().count();
    println!("counted {num_lines} lines");
    let reader = buf_reader.into_inner();
    println!("exit status: {:?}", reader.try_wait()?.unwrap());
    Ok(())
}
oconnor663 commented 5 months ago

That said, ReaderHandle is designed so that this usually isn't necessary. Take a look at the docs:

When this reader reaches EOF, it automatically calls wait on the inner handle. If the child returns a non-zero exit status, the read at EOF will return an error, unless you use unchecked.

So if what you want is to read the handle all the way to the end and then return an error if the child's status was non-zero, that's what happens automatically! Take a look at this example:

use duct::cmd;
use std::io::prelude::*;
use std::io::BufReader;

const PYTHON: &str = r#"
print("one")
print("two")
print("three")
assert 1 == 2  # raise an exception and ultimately exit with a non-zero status
"#;

fn main() -> anyhow::Result<()> {
    let reader = cmd!("python3", "-c", PYTHON)
        .env("PYTHONUNBUFFERED", "1")
        .stderr_to_stdout()
        .reader()?;
    for line in BufReader::new(reader).lines() {
        println!("read a line: {:?}", line?);
    }
    println!("We never get here!");
    Ok(())
}
$ cargo run
cargo run         
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/scratch`
read a line: "one"
read a line: "two"
read a line: "three"
read a line: "Traceback (most recent call last):"
read a line: "  File \"<string>\", line 5, in <module>"
read a line: "AssertionError"
Error: command ["python3", "-c", "\nprint(\"one\")\nprint(\"two\")\nprint(\"three\")\nassert 1 == 2  # raise an exception and ultimately exit with a non-zero status\n"] exited with code 1
$ echo $?
1