Spawning External Commands
Spawning external commands creates a new operating system child process to execute an external program. Rust's std::process::Command provides three distinct execution methods—status(), output(), and spawn()—that differ in whether they block the parent thread and how they connect the child's standard I/O streams.
Command::new("program")
|
+--------------------------+--------------------------+
| | |
v v v
.status() .output() .spawn()
--------- --------- --------
Blocks until exit Blocks until exit Returns immediately!
Returns: io::Result< Returns: io::Result< Returns: io::Result<
ExitStatus> Output> Child>
Default Stdio: Default Stdio: Default Stdio:
stdin: Inherit stdin: Null stdin: Inherit
stdout: Inherit stdout: Piped stdout: Inherit
stderr: Inherit stderr: Piped stderr: Inherit
Comparing status(), output(), and spawn()
| Method | Return Type | Blocking? | Default stdin |
Default stdout / stderr |
Primary Use Case |
|---|---|---|---|---|---|
status() |
io::Result<ExitStatus> |
Yes | Stdio::inherit() |
Stdio::inherit() |
Running a command whose output should stream directly to the user's terminal, checking only whether it succeeded. |
output() |
io::Result<Output> |
Yes | Stdio::null() |
Stdio::piped() |
Running a command to capture its complete stdout and stderr buffers into memory. |
spawn() |
io::Result<Child> |
No | Stdio::inherit() |
Stdio::inherit() |
Running a background process, streaming pipes interactively, or running multiple children concurrently. |
Capturing Output with .output()
Command::output() waits for the child process to finish and collects all bytes written to its standard output and standard error streams into Output { status, stdout, stderr }.
use std::io;
use std::process::Command;
fn main() -> io::Result<()> {
let output = Command::new("rustc")
.arg("--version")
.output()?;
if output.status.success() {
let text = String::from_utf8_lossy(&output.stdout);
println!("Captured stdout ({} bytes): {}", output.stdout.len(), text.trim());
} else {
let err = String::from_utf8_lossy(&output.stderr);
eprintln!("Command failed: {err}");
}
Ok(())
}
Captured stdout (35 bytes): rustc 1.85.0 (4d91de4e4 2025-02-17)
Non-Blocking Execution with .spawn()
Command::spawn() starts the child process and returns a Child handle immediately so the parent can perform concurrent work, poll with try_wait(), or wait later with wait().
use std::io;
use std::process::{Command, Stdio};
fn main() -> io::Result<()> {
let mut child = Command::new("rustc")
.arg("-V")
.stdout(Stdio::null())
.spawn()?;
// Poll without blocking via `try_wait()`
match child.try_wait()? {
Some(status) => println!("Already exited with: {status}"),
None => {
// Still running; block until completion
let final_status = child.wait()?;
println!("Finished after wait, success: {}", final_status.success());
}
}
Ok(())
}
Finished after wait, success: true
Avoiding Shell Injection
Command::new executes the target binary directly via the OS kernel (posix_spawnp / fork+execvp on Unix, CreateProcessW on Windows) without invoking a system shell (/bin/sh or cmd.exe).
Because no shell interprets the arguments:
- Shell metacharacters such as
;,|,&&,$(...), and>passed inside.arg()are delivered verbatim as literal strings to the target executable, eliminating shell command injection vulnerabilities. - Shell built-ins (such as
cd,export, ordiron Windows) and glob patterns (*.rs) are not expanded automatically unless you explicitly invokesh -corcmd /C.