Martin Cowie
Martin Cowie

Reputation: 2601

What is `take()` doing here, and why do I need it?

This code spawns a child process, consuming its stderr and stdout line by line, and logging each appropriately. It compiles and works.

use std::error::Error;
use std::process::{Stdio};
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::{Command, Child};
use tracing::{info, warn};

macro_rules! relay_pipe_lines {
    ($pipe:expr, $handler:expr) => {
        tokio::spawn(async move {
            let mut reader = BufReader::new($pipe).lines();

            loop {
                let line = reader
                    .next_line()
                    .await
                    .unwrap_or_else(|_| Some(String::new()));

                match line {
                    None => break,
                    Some(line) => $handler(line)
                }

            }
        });
    };
}

pub fn start_and_log_command(mut command: Command) -> Result<Child, Box<dyn Error>> {
    command.stdout(Stdio::piped()).stderr(Stdio::piped());

    let mut child = command.spawn()?;

    let child_stdout = child.stdout.take().unwrap(); // remove `take` from here
    let child_stderr = child.stderr.take().unwrap(); // .. or from here and it fails
    let child_pid = child.id().unwrap();

    relay_pipe_lines!(child_stdout, |line|info!("[pid {}:stdout]: {}", child_pid, line));
    relay_pipe_lines!(child_stderr, |line|warn!("[pid {}:stderr]: {}", child_pid, line));

    Ok(child)
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    tracing_subscriber::fmt::init();
    info!("Tracing logging initialised.");

    let mut command = Command::new("ls");
    command.arg("-l");
    let mut child = start_and_log_command(command)?;

    // Compose reading waiting concurrently.
    let exit_status = child.wait().await.expect("Cannot reap child process");

    dbg!(exit_status.success());

    Ok(())
}

Removing the call to take() from the indicated lines fails the build, as "child.stdout partially moved due to this method call", which I mostly understand.

I'd like to understand how using take() does not partially move child.stdout.

Upvotes: 1

Views: 547

Answers (1)

user4815162342
user4815162342

Reputation: 155196

It's a call to Option::take(), which avoids a "partial move" by leaving None in place of the moved value. As a result, child is left in a valid state and can be returned from the function.

In other words, child.stdout.take() is equivalent to std::mem::replace(&mut child.stdout, None), and means "take the current value out of the option (whatever it is), and leave None in its place."

Upvotes: 10

Related Questions