`take()` preventing output from being captured

If you run my code, "test" will be printed (in real time), but stdout won't capture the output of the command:

test
Ok(Output { status: ExitStatus(unix_wait_status(0)), stdout: "", stderr: "" })

If you remove or comment out the code between ADDED, stdout will capture the output of the command, but "test" won't be printed (in real time). Actually, it won't be printed at all:

Ok(Output { status: ExitStatus(unix_wait_status(0)), stdout: "test\n", stderr: "" })

Full code:

use std::{
    io::{BufRead, BufReader},
    process::{Command, Output, Stdio},
};

fn run_with_output() -> Result<Output, std::io::Error> {
    let mut command = Command::new("sh");

    command
        .arg("-c")
        .arg("sleep 2 && echo test && sleep 2")
        .stdout(Stdio::piped());

    let mut child = command.spawn().expect("failed to spawn command");
    
    // ADDED start
    let stdout_pipe = child.stdout.take().unwrap();
    let stdout_reader = BufReader::new(stdout_pipe);

    let stdout_thread = std::thread::spawn(move || {
        for line in stdout_reader.lines() {
            if let Ok(line) = line {
                print!("{}\r\n", line);
            }
        }
    });

    stdout_thread.join().expect("failed to join stdout thread");
    // ADDED end
    
    let output = child.wait_with_output()?;

    Ok(output)
}

fn main() {
    let output = run_with_output();

    println!("{:?}", output);
}

Rust Explorer

How to change this code so that "test" is printed (in real time) and stdout captures the output of the command (which is also "test")?

The thing that wait_with_output() would do when it succeeds is basically your stdout_thread. In order to get the output in two places, your stdout_thread must copy the output to two places.

Here is a revised version of your program that does that (and I made a few other cleanups too):

use std::{
    io::{BufRead, BufReader},
    process::{Command, Stdio},
};

fn run_with_output() -> Result<String, std::io::Error> {
    let mut child = Command::new("sh")
        .arg("-c")
        .arg("sleep 2 && echo test && sleep 2")
        .stdout(Stdio::piped())
        .spawn()
        .expect("failed to spawn command");

    let stdout_pipe = child.stdout.take().unwrap();
    let stdout_thread = std::thread::spawn(move || {
        let mut capture = String::new();
        for line in BufReader::new(stdout_pipe).lines() {
            let line = line.unwrap();
            capture.push_str(&line);
            println!("{line}");
        }
        capture
    });

    let output: String = stdout_thread.join().expect("failed to join stdout thread");

    Ok(output)
}

fn main() {
    let output = run_with_output();

    println!("{:?}", output);
}

Note: In case the child prints any partial lines (e.g. progress reporting) it would be good to avoid lines() and copy the output byte-by-byte instead. I did not make that change.

1 Like

Thanks a lot! I can't believe the answer was so simple.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.