Running other programs
A Rust program starts another one with std::process::Command. The builder takes the program and its arguments, and the three terminal methods decide how much of the child the parent wants: its status, its collected output, or the pipes to read while it runs.
This lesson runs a command and reads its output, looks at a status, reads a child line by line, and collects both streams from a shell. The last section is the compiler refusing to compare an ExitStatus with zero.
A command and its output
output runs the child to completion and collects its standard output and standard error into vectors of bytes. The bytes are not text until the program decides they are, which is why the print uses from_utf8_lossy.
The program runs echo with one argument and prints the status and the line. Both are what the child produced.
use std::process::Command;
fn main() {
let output = Command::new("echo").arg("hello from a child").output().unwrap();
println!("{}", output.status.success());
println!("{}", String::from_utf8_lossy(&output.stdout).trim_end());
}
rustc --edition 2024 run.rs
./run
true
hello from a child
The trim_end removes the newline the child wrote, which is part of its output and not of the text. The status is a value and not a number, which the last section turns into an error. The arguments lesson is the other side of the same interface.
The exit status
status runs the child and returns an ExitStatus without collecting the output, which is what a program that only cares about success uses. success is true for zero and code gives the number when the child exited normally.
The program runs false, which exits with one. The status says the failure and reports the code.
use std::process::Command;
fn main() {
let status = Command::new("false").status().unwrap();
println!("{} {:?}", status.success(), status.code());
}
rustc --edition 2024 status.rs
./status
false Some(1)
code returns Option<i32> because a child killed by a signal has no exit code, only a signal number. That is the same distinction the panic lesson makes for a Rust process that aborts.
Reading the output line by line
A child that produces a lot of output is read while it runs. Stdio::piped gives the parent the reading end, stdout.take() takes it out of the child, and a BufReader turns the bytes into lines.
The program runs printf with two lines, prints each with a number and then waits for the child to finish.
use std::io::{BufRead, BufReader};
use std::process::{Command, Stdio};
fn main() {
let mut child = Command::new("printf")
.arg("alpha\nbeta\n")
.stdout(Stdio::piped())
.spawn()
.unwrap();
let stdout = child.stdout.take().unwrap();
let reader = BufReader::new(stdout);
let mut lines = 0;
for line in reader.lines() {
let line = line.unwrap();
lines += 1;
println!("line {lines}: {line}");
}
let status = child.wait().unwrap();
println!("{}", status.success());
}
rustc --edition 2024 pipe.rs
./pipe
line 1: alpha
line 2: beta
true
The loop ends when the pipe closes, which happens when the child exits, and then wait collects the status. Reading before the wait is what keeps a child with a lot of output from blocking on a full pipe. The lesson on text files reads the same lines from a file.
A child that is spawned and never waited for leaves a zombie entry in the process table until the parent ends. wait cleans it, and a program that starts many children keeps the handles and waits in a loop. kill sends a signal to a running child, which is how a timeout is enforced: read with a deadline, kill on expiry, then wait for the status.
Both streams and a status
A shell command writes to both streams and fails on purpose. output collects the two of them and waits, so the program can print the code, the text and the error in one go.
The child is sh -c with three commands: one line to the standard output, one to the standard error and an exit status of three.
use std::process::Command;
fn main() {
let output = Command::new("sh")
.arg("-c")
.arg("echo out; echo err >&2; exit 3")
.output()
.unwrap();
println!("{}", output.status.code().unwrap());
println!("{}", String::from_utf8_lossy(&output.stdout).trim_end());
println!("{}", String::from_utf8_lossy(&output.stderr).trim_end());
}
rustc --edition 2024 child.rs
./child
3
out
err
The three lines are the three facts about the child, and the parent decides which of them to show. A program that passes the status on returns the same number from its own main, which is how a wrapper keeps the contract of the tool it wraps.
A child that is spawned and never waited for leaves a zombie entry in the process table until the parent ends. wait cleans it, and a program that starts many children keeps the handles and waits in a loop. kill sends a signal to a running child, which is how a timeout is enforced: read with a deadline, kill on expiry, then wait for the status.
The status of the child is the value a wrapper returns, so a missing program and a failing program stay two different cases in the report.
A status compared with a number
ExitStatus is a struct and not an integer, so status == 0 does not compile. The compiler reports E0308, names the struct and tells the program to ask for success or for the code.
The program compares the status with zero. The compiler points at the literal.
use std::process::Command;
fn main() {
let status = Command::new("true").status().unwrap();
if status == 0 {
println!("ok");
}
}
rustc --edition 2024 compare_err.rs
error[E0308]: mismatched types
--> compare_err.rs:5:18
|
5 | if status == 0 {
| ------ ^ expected `ExitStatus`, found integer
| |
| expected because this is `ExitStatus`
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0308`.
The two replacements are status.success() and status.code() == Some(0), and the first is the one to write. A program that runs a command that does not exist gets an Err from the spawn instead of a status, and unwrapping it panics with the operating system message: kind: NotFound, message: "No such file or directory". A later lesson times the work these commands do.
Exercises
- Run ls with a directory argument and print the number of lines.
- Pass the environment of the parent to a child with envs and one extra variable.
- Return the child's exit code from the parent with ExitCode::from.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does
Command::outputreturn?Answer
A result with the status, the standard output and the standard error
The output struct holds all three, collected after the child exits.
-
Why is the output a vector of bytes?
Answer
A program may write text that is not UTF-8
The bytes are only text when the program decides they are valid Unicode.
-
What does
status.code()return for a child killed by a signal?Answer
None
There is no exit code in that case, only the signal that ended the process.
-
Why is the child stdout taken with
take()before reading?Answer
The pipe is an Option field, and reading needs to own it
The parent takes the reading end out of the child handle to read it.
-
What does this program print?
use std::process::Command; fn main() { let output = Command::new("printf").arg("ab").output().unwrap(); println!("{}", output.stdout.len()); }Answer
2
The child writes two bytes, and the vector has two elements.
-
What does this program print?
use std::process::Command; fn main() { let status = Command::new("true").status().unwrap(); println!("{}", status.success()); }Answer
true
The true command exits with zero, which success reports.
-
What does this program print?
use std::process::Command; fn main() { let output = Command::new("sh").arg("-c").arg("echo hi").output().unwrap(); println!("{}", String::from_utf8_lossy(&output.stdout).trim_end()); }Answer
hi
The trim removes the newline and the text is printed.
-
What does this program print?
use std::process::Command; fn main() { let status = Command::new("sh").arg("-c").arg("exit 4").status().unwrap(); println!("{:?}", status.code()); }Answer
Some(4)
The shell exits with four, which the code reports.
-
What is the first line of the compiler output for this program?
use std::process::Command; fn main() { let status = Command::new("true").status().unwrap(); if status == 0 { println!("ok"); } }Answer
error[E0308]: mismatched types
The comparison expects an ExitStatus and receives an integer literal.
-
What is the first line of the compiler output for this program?
use std::process::Command; fn main() { let output = Command::new("echo").arg("x").output().unwrap(); println!("{}", output.stdout.trim_end()); }Answer
error[E0599]: no method named
trim_endfound for structVec<u8>in the current scopeThe output is a vector of bytes, and the text methods belong to a string slice.
The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev