Environment and Process Management

Standard Input and Output of Child Processes

Every child process starts with three standard I/O descriptors—standard input (stdin), standard output (stdout), and standard error (stderr)—configured via std::process::Stdio. Connecting these streams to OS pipes, files, or null devices enables bidirectional communication and multi-stage process pipelines.

Parent Process                                        Child Process
+-----------------------------+                       +-------------------+
| child.stdin:  ChildStdin  --+---[ OS Pipe Buffer ]->| fd 0 (stdin)      |
|                             |                       |                   |
| child.stdout: ChildStdout <-+---[ OS Pipe Buffer ]--| fd 1 (stdout)     |
|                             |                       |                   |
| child.stderr: ChildStderr <-+---[ OS Pipe Buffer ]--| fd 2 (stderr)     |
+-----------------------------+                       +-------------------+

Stdio Configuration Options

Constructor Behavior Handle on Child
Stdio::inherit() Child shares the parent's stdin, stdout, or stderr descriptor directly. None
Stdio::piped() Creates an OS pipe connecting the parent and child. Some(ChildStdin) / Some(ChildStdout) / Some(ChildStderr)
Stdio::null() Connects the stream to /dev/null (Unix) or NUL (Windows), discarding output or returning immediate EOF on read. None
Stdio::from(file) Redirects the stream directly to or from an open std::fs::File, ChildStdout, ChildStderr, or OwnedFd. None

Writing to ChildStdin and Reading from ChildStdout

When a child process reads from stdin until EOF before producing output (or while producing output), the parent must take and drop child.stdin after writing so the child receives an EOF signal. Furthermore, if a child writes more data to stdout than fits in the OS pipe buffer (typically 16–64 KB) while the parent is still blocked writing a large payload to stdin, both processes can deadlock unless stdin is written on a separate thread.

use std::io::{self, Write};
use std::process::{Command, Stdio};
use std::thread;

fn main() -> io::Result<()> {
    // Compile any stdin source directly using `rustc - --crate-type lib`
    let mut child = Command::new("rustc")
        .args(["-", "--crate-type", "lib", "-Z", "unpretty=normal"])
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()?;

    // Take ownership of `ChildStdin` so it is closed (dropped) when the writer finishes
    let mut stdin = child.stdin.take().expect("stdin was piped");

    let writer = thread::spawn(move || -> io::Result<()> {
        stdin.write_all(b"pub fn add(a: i32, b: i32) -> i32 { a + b }\n")?;
        Ok(()) // `stdin` drops here, closing the pipe and sending EOF to child
    });

    // Wait for child and read all stdout/stderr concurrently
    let _output = child.wait_with_output()?;
    writer.join().expect("writer thread panicked")?;

    println!("Pipe write and child wait completed without deadlock");
    Ok(())
}
Pipe write and child wait completed without deadlock

Redirecting Child Output Directly to a File

Because Stdio implements From<File>, you can redirect a child's stdout or stderr straight to a file at the OS file-descriptor level without copying bytes through user-space buffers in the parent process:

use std::env;
use std::fs::{self, File};
use std::io;
use std::process::{Command, Stdio};

fn main() -> io::Result<()> {
    let log_path = env::temp_dir().join("rust_ch51_rustc_ver.log");
    let log_file = File::create(&log_path)?;

    let status = Command::new("rustc")
        .arg("--version")
        .stdout(Stdio::from(log_file))
        .status()?;

    assert!(status.success());
    let logged = fs::read_to_string(&log_path)?;
    println!("File redirection captured: {}", logged.trim());

    fs::remove_file(&log_path)?;
    Ok(())
}
File redirection captured: rustc 1.85.0 (4d91de4e4 2025-02-17)

Chaining Two Child Processes into a Pipeline

ChildStdout also implements Into<Stdio>, allowing the stdout pipe of one child process to be wired directly into the stdin of a second child process (equivalent to cmd1 | cmd2 in a shell):

let mut first = Command::new("producer").stdout(Stdio::piped()).spawn()?;
let pipe_out: Stdio = first.stdout.take().unwrap().into();
let second = Command::new("consumer").stdin(pipe_out).output()?;
first.wait()?;