Open ezntek opened 9 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(())
}
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
)
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(())
}
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 useunchecked
.
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
Is it possible for me to call
.stderr_to_stdout()
on an expression and also get an exit status?