How to run an external command and read its output
Short answer: Command::new("git").args(["status"]).output() runs the command, waits for it to exit, and returns everything it produced in one struct: the exit status, the bytes the child wrote to stdout, and the bytes it wrote to stderr. A nonzero exit is not an Err — it arrives in the status field. Err is reserved for the case where the command could not be started at all.
Everything below uses commands that exist on any macOS or Linux machine: echo and /bin/sh. The sh -c '...' form is a way to run a small shell snippet as the child, so the examples can fail, exit with a chosen code, or get killed without installing anything.
One call, one struct
Section titled “One call, one struct”use std::process::Command;
fn main() { let output = Command::new("echo") .arg("hello from a child process") .output() .expect("failed to run echo");
println!("status: {}", output.status); println!("stdout: {}", String::from_utf8_lossy(&output.stdout)); println!("stderr: {} bytes", output.stderr.len());}status: exit status: 0stdout: hello from a child process
stderr: 0 bytesCommand::output() is the whole API. It spawns the child, connects pipes to all three standard streams, waits for the child to exit, and hands back a std::process::Output with three public fields:
status— anExitStatus, which prints asexit status: 0herestdout— aVec<u8>of everything the child wrote to stdoutstderr— the same for stderr
The blank line after stdout: is not noise. echo writes its argument followed by a newline, so the captured stdout ends in \n — and println! adds another one. A child’s output usually ends in a newline; trim it with .trim_end() when comparing or printing it as one line.
The child’s output never reaches your terminal
Section titled “The child’s output never reaches your terminal”Without .output(), a child inherits your stdout and its output appears as if your own program printed it. With .output(), the child is redirected: everything it writes goes into the buffers, and nothing appears until your code prints it.
use std::process::Command;
fn main() { let output = Command::new("echo") .arg("this never reaches the terminal") .output() .expect("failed to run echo");
print!("{}", String::from_utf8_lossy(&output.stdout));}this never reaches the terminalThe line appears exactly once, and only because the last line prints it. If you capture the output and forget to print it, it is simply gone. This is the most common reason captured output “disappears”: the program ran the command, kept the bytes, and never wrote them anywhere.
A nonzero exit is not an Err
Section titled “A nonzero exit is not an Err”output() returns Ok as long as the child started — even if it exits with an error code. The failure lives in status:
use std::process::Command;
fn main() { let output = Command::new("sh") .args(["-c", "echo to stdout; echo to stderr >&2; exit 3"]) .output() .expect("failed to run sh");
println!("success: {}", output.status.success()); println!("code: {:?}", output.status.code()); println!("stdout: {}", String::from_utf8_lossy(&output.stdout)); println!("stderr: {}", String::from_utf8_lossy(&output.stderr));}success: falsecode: Some(3)stdout: to stdout
stderr: to stderrThe shell wrote one line to each stream and exited with code 3, and all three facts come back separately: status.success() is the quick true/false check, status.code() is the Option<i32> number, and the two streams arrive already separated. Note the same trailing-newline effect on both captured lines — each echo left a \n, and println! added another.
If the child exits because a signal killed it — the Unix case, not portable to Windows — code() returns None and the Display of the status says which signal:
use std::process::Command;
fn main() { let output = Command::new("sh") .args(["-c", "kill -TERM $$"]) .output() .expect("failed to run sh");
println!("status: {}", output.status); println!("code: {:?}", output.status.code());}status: signal: 15 (SIGTERM)code: Nonekill -TERM $$ sends SIGTERM to the shell’s own process, so the child dies by signal rather than exiting normally. That is why code() is None: there is no exit code, there is a signal. Code that checks only code() == Some(0) treats a signal-killed child the same as a successful one — check success() instead, which is false for both a nonzero exit and a signal death.
stdout is bytes, not a String
Section titled “stdout is bytes, not a String”The stdout and stderr fields are Vec<u8>, because command output has no guarantee of being UTF-8. String::from_utf8_lossy turns the bytes into something printable, replacing invalid sequences with the replacement character:
use std::process::Command;
fn main() { let output = Command::new("sh") .args(["-c", "printf 'caf\\xff'"]) .output() .expect("failed to run sh");
println!("{} bytes: {:?}", output.stdout.len(), output.stdout); println!("lossy: {}", String::from_utf8_lossy(&output.stdout));}4 bytes: [99, 97, 102, 255]lossy: caf�The 0xff byte survives intact in the Vec<u8> — it is not valid UTF-8, so the lossy conversion turns it into �. If you need the strict version, String::from_utf8(output.stdout) returns a Result instead of inventing replacement characters.
When you do not want the output: .status()
Section titled “When you do not want the output: .status()”Capturing is not always what you want. An interactive child — an editor, a pager — needs its output to go straight to the terminal, not into a buffer. .status() runs the same command with inherited streams and returns only the ExitStatus:
use std::process::Command;
fn main() { let status = Command::new("sh") .args(["-c", "echo straight to the terminal; exit 2"]) .status() .expect("failed to run sh");
println!("success: {}", status.success());}straight to the terminalsuccess: falseThe child’s line appears even though the program never prints it — with .status() there is no redirection at all. The rule of thumb: .status() when the user should see the child, .output() when the program needs the bytes.
Two things .output() does not do
Section titled “Two things .output() does not do”- It does not stream.
output()waits for the child to exit and returns everything at once. Reading a child’s output while it is still running means piping streams yourself — that is its own article in this chapter. - It does not find the command. A name that is not on
PATHnever starts, and that comes back asErr— the next article covers what that error looks like and how to check first.
Related
Section titled “Related”- How to set an environment variable for a child process — the environment the child above inherited
- How to read environment variables — the other side: reading variables inside your own process
- Processes, environment and signals — the rest of the chapter