Question
In Rust, how can I run a system command such as ls or fuser? I also want to know how to capture the command's output, including standard output and possibly other result details such as the exit status.
Short Answer
By the end of this page, you will understand how to run external system commands in Rust using std::process::Command, how to capture their output, how to check whether they succeeded, and how to handle common errors safely in real programs.
Concept
Rust provides the std::process::Command type for launching external programs. This is the standard way to invoke system commands such as ls, fuser, grep, or any executable available on the system.
When you run an external command, there are a few important parts to understand:
- The program: the executable you want to run
- Arguments: the values passed to that program
- Exit status: whether the command succeeded or failed
- Standard output (
stdout): normal output from the command - Standard error (
stderr): error messages from the command
In Rust, Command lets you:
- start a command and wait for it to finish
- capture its output
- pass arguments safely
- configure input and output streams
- inspect the exit code
This matters in real programming because many tools and workflows depend on external programs. For example:
- calling Git from a Rust CLI tool
- invoking image or video processing tools
- running shell utilities in deployment scripts
- integrating with system-level commands on Linux or macOS
A key detail is that Command runs a program directly. It does not automatically run through a shell like Bash. That means shell features such as pipes (|), wildcards (*), and redirection (>) do not work unless you explicitly invoke a shell yourself.
Mental Model
Think of Command as filling out a form to ask the operating system to launch another program.
- The program name is the person you want to contact.
- The arguments are the instructions you give them.
- stdout is their normal reply.
- stderr is their complaint or warning.
- The exit status is their final signal saying whether the task succeeded.
Rust acts like a careful assistant: instead of sending one big shell string, it keeps the program and each argument separate. This avoids many quoting and escaping mistakes.
Syntax and Examples
The most common API is std::process::Command.
Basic syntax
use std::process::Command;
fn main() {
let status = Command::new("ls")
.arg("-l")
.status()
.expect("failed to run ls");
println!("Exit status: {status}");
}
Command::new("ls")chooses the program.arg("-l")adds one argument.status()runs the command and waits for it to finish.expect(...)handles the case where the program could not be started
Capture command output
use std::process::Command;
fn main() {
let output = Command::new("ls")
.arg("-l")
.output()
.expect("failed to run ls");
println!("Success: {}", output.status.success());
println!("stdout:\n{}", String::from_utf8_lossy(&output.stdout));
println!("stderr:\n{}", String::from_utf8_lossy(&output.stderr));
}
What this does
.output()runs the command and captures bothstdoutandstderroutput.stdoutandoutput.stderrare byte arrays (Vec<u8>)String::from_utf8_lossy(...)converts bytes into printable text safely
Pass multiple arguments
use std::process::Command;
fn main() {
let output = Command::new("fuser")
.args(["-v", "/tmp"])
.output()
.expect("failed to run fuser");
println!("{}", String::from_utf8_lossy(&output.stdout));
}
Use .args([...]) when you have several arguments.
Handle success and failure
use std::process::Command;
fn main() {
let output = Command::new("ls")
.arg("/path/that/does/not/exist")
.output()
.expect("failed to run ls");
if output.status.success() {
println!("Command worked");
println!("{}", String::from_utf8_lossy(&output.stdout));
} else {
eprintln!("Command failed with status: {}", output.status);
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
}
}
This is the usual pattern in real code: check the exit status before trusting the output.
Step by Step Execution
Consider this example:
use std::process::Command;
fn main() {
let output = Command::new("echo")
.arg("hello")
.output()
.expect("failed to run echo");
let text = String::from_utf8_lossy(&output.stdout);
println!("Captured: {}", text);
}
Here is what happens step by step:
-
Command::new("echo")- Rust prepares to run the external program named
echo.
- Rust prepares to run the external program named
-
.arg("hello")- One argument is added, so the final command is effectively
echo hello.
- One argument is added, so the final command is effectively
-
.output()- Rust starts the program.
- Rust waits until it finishes.
- Rust collects:
- the exit status
- standard output
- standard error
-
.expect("failed to run echo")- If Rust cannot start the program at all, the program stops with this message.
- This is different from the command itself returning an error status.
-
String::from_utf8_lossy(&output.stdout)- The captured bytes from
stdoutare converted into readable text.
- The captured bytes from
-
println!("Captured: {}", text)- The output is printed by the Rust program.
If echo prints hello, then output.stdout contains the bytes for hello\n.
A useful distinction:
- Launch failure: the executable could not be found or started
- Command failure: the executable ran, but returned a non-zero exit code
Real World Use Cases
External commands are often used when Rust programs need to interact with the operating system or existing tools.
Common uses
-
CLI wrappers
- A Rust tool may call
git,docker, orkubectland process the result.
- A Rust tool may call
-
System administration scripts
- Run commands like
ls,ps,df, orfuserand inspect their output.
- Run commands like
-
Build and deployment tools
- Execute external build steps, package managers, or shell utilities.
-
Data processing pipelines
- Use existing tools for search, compression, or file conversion.
-
Media and document processing
- Call tools like
ffmpeg,pandoc, or ImageMagick from Rust.
- Call tools like
Example: checking Git version
use std::process::Command;
fn main() {
let output = Command::new("git")
.arg("--version")
.output()
.expect("failed to run git");
if output.status.success() {
println!("{}", String::from_utf8_lossy(&output.stdout));
} else {
eprintln!("git returned an error");
}
}
This pattern is common in developer tools and automation scripts.
Real Codebase Usage
In real Rust codebases, developers usually wrap command execution in helper functions so the rest of the application stays clean and testable.
Common patterns
Guard clauses for failure
Check whether the command launched and whether it succeeded before using the output.
use std::process::Command;
fn run_ls() -> Result<String, String> {
let output = Command::new("ls")
.arg("-l")
.output()
.map_err(|e| format!("Could not start ls: {e}"))?;
if !output.status.success() {
return Err(String::from_utf8_lossy(&output.stderr).into_owned());
}
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
Validation before execution
Programs often validate user input before passing it to a command.
- check file paths
- check required arguments
- avoid building unsafe shell strings
Returning structured results
Instead of returning only text, a project may return a struct containing:
- exit status
- stdout
- stderr
Logging failures
When command execution fails, developers often log:
- which command was run
- which arguments were used
- the exit status
- stderr output
Avoiding the shell when possible
Real projects usually prefer:
Command::new("grep").args(["foo", "file.txt"])
instead of:
Command::new("sh").args(["-c", "grep foo file.txt"])
The direct version is safer and easier to reason about because each argument is explicit.
Common Mistakes
Beginners often confuse process execution with shell execution. Here are the most common problems.
1. Expecting shell syntax to work automatically
This does not work as many beginners expect:
use std::process::Command;
fn main() {
let output = Command::new("ls -l")
.output()
.expect("failed");
println!("{:?}", output);
}
Why it is wrong
Command::new expects the executable name only. It tries to find a program literally named ls -l.
Correct version
use std::process::Command;
fn main() {
let output = Command::new("ls")
.arg("-l")
.output()
.expect("failed");
println!("{}", String::from_utf8_lossy(&output.stdout));
}
2. Forgetting that output is bytes
Broken assumption:
// output.stdout is not a String
You must convert bytes to text:
let text = String::from_utf8_lossy(&output.stdout);
3. Ignoring stderr
Some commands write useful information or errors to stderr, not stdout.
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
4. Not checking the exit status
A command can run successfully from Rust's point of view but still fail as a command.
let output = Command::new("ls")
.arg("missing-file")
.output()
.expect("failed to start command");
if !output.status.success() {
eprintln!("Command failed");
}
5. Using a shell when it is unnecessary
Running through sh -c or cmd /C is more error-prone and can be unsafe if user input is inserted into the command string.
Prefer separate arguments whenever possible.
6. Assuming commands exist on every platform
lsis common on Unix-like systems- Windows typically uses different commands
If you need portability, choose commands and paths carefully.
Comparisons
| Approach | What it does | Captures output? | Best use |
|---|---|---|---|
Command::new(...).status() | Runs a command and returns only the exit status | No | When you only care whether it succeeded |
Command::new(...).output() | Runs a command and captures stdout, stderr, and status | Yes | When you need to read command output |
Command::new(...).spawn() | Starts a command without waiting immediately | Not by default | Long-running processes or interactive control |
status() vs output()
use std::process::Command;
fn main() {
let status = Command::new("ls")
.status()
.expect("failed to start");
println!("Status: {status}");
}
Use status() when printing directly to the terminal is fine.
use std::process::Command;
fn main() {
let output = Command::new("ls")
.output()
.expect("failed to start");
println!("{}", String::from_utf8_lossy(&output.stdout));
}
Use output() when you need the text inside your Rust code.
Direct command vs shell command
| Style | Example | Safer? | Notes |
|---|---|---|---|
| Direct execution | Command::new("ls").arg("-l") | Yes | Preferred for most cases |
| Shell execution | `Command::new("sh").args(["-c", "ls -l | grep txt"])` | No |
Cheat Sheet
Quick reference
Run a command
use std::process::Command;
let status = Command::new("ls")
.arg("-l")
.status()?;
Capture output
use std::process::Command;
let output = Command::new("ls")
.arg("-l")
.output()?;
Read stdout and stderr
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
Check success
if output.status.success() {
// command succeeded
}
Multiple arguments
Command::new("fuser").args(["-v", "/tmp"]);
Important rules
Command::new()takes the program name only- Use
.arg()or.args()for arguments .output()capturesstdout,stderr, and exit status.status()gives only the exit status- Output is returned as bytes, not
String - Shell syntax like
|,>, and*does not work unless you explicitly invoke a shell
Common edge case
If the command cannot be found, Rust returns an error before the command even runs.
FAQ
How do I run a terminal command in Rust?
Use std::process::Command. Create a command with Command::new("program"), add arguments with .arg() or .args(), then call .status() or .output().
How do I capture stdout from a command in Rust?
Call .output() and read output.stdout. Convert it to text with String::from_utf8_lossy(&output.stdout).
How do I capture stderr in Rust?
output.stderr contains the captured error output. Convert it the same way as stdout.
What is the difference between status() and output() in Rust?
status() returns only the exit status. output() returns the exit status plus captured stdout and stderr.
Why does Command::new("ls -l") fail?
Because Rust looks for an executable literally named ls -l. Pass the program and its arguments separately: Command::new("ls").arg("-l").
Can I run shell features like pipes or redirection with Command?
Not directly. Command runs programs without a shell. For shell syntax, explicitly run a shell such as sh -c on Unix.
How do I know whether the command succeeded?
Check output.status.success() or status.success().
Is Command cross-platform?
The API is cross-platform, but the commands you run may not be. For example, ls is common on Unix-like systems but not standard on Windows.
Mini Project
Description
Build a small Rust utility that runs a system command entered in code, captures its output, and reports whether it succeeded. This demonstrates the core workflow of launching a child process, reading stdout and stderr, and checking the exit status.
Goal
Create a Rust program that runs a command, captures its output, and prints a clear success or failure report.
Requirements
- Run an external command using
std::process::Command. - Pass at least one argument to the command.
- Capture both
stdoutandstderr. - Print whether the command succeeded.
- Show the captured output in a readable text form.
Keep learning
Related questions
Absolute Values in Rust: Using the abs() Method
Learn how to calculate absolute values in modern Rust with abs(), handle signed integer overflow, and choose safer alternatives.
Accepting Iterators in Rust Functions with impl Iterator
Learn how to write Rust functions that accept HashMap iterators using generic bounds or impl Iterator, avoiding trait object and lifetime errors.
Access Enum Variant Values in Rust with Pattern Matching
Learn how to extract and print values stored in Rust enum variants using match and if let pattern matching, with practical examples.