Rust allows you to execute system commands through the std::process::Command API. You can spawn external programs, pass arguments, capture output, and handle errors, all using safe and structured code.
One of the first CLI utilities I built in Rust needed to call an external tool installed on the system. I was worried it would require unsafe code or complex APIs. But Rust’s Command interface turned out to be clean, flexible, and surprisingly easy to use. It gave me full control over arguments, output, and error handling.
In this article, you will learn how to run external commands, capture their output, handle failures, and build reliable shell wrappers using Rust.
Basic Usage with Command
To run a command like ls, you can use:
use std::process::Command;
fn main() {
let status = Command::new("ls")
.status()
.expect("Failed to run command");
println!("Exit status: {}", status);
}
This spawns the command and returns a status code. It does not capture the output, it just runs the process and shows its result.
Passing Arguments
You can add arguments using arg or args:
let status = Command::new("echo")
.arg("Hello")
.arg("Rust")
.status()
.expect("Failed to run echo");
This executes echo Hello Rust.
Use args(&[…]) to pass a slice of arguments.
Capturing Output
Use output() to capture standard output and standard error:
let output = Command::new("echo")
.arg("Rust is fast")
.output()
.expect("Failed to run echo");
let stdout = String::from_utf8_lossy(&output.stdout);
println!("stdout: {}", stdout);
This gives you the output as bytes. Use from_utf8_lossy to convert it into a readable String.
Capturing Standard Error
You can also access stderr:
let error_output = String::from_utf8_lossy(&output.stderr);
println!("stderr: {}", error_output);
Useful when the command fails or returns warnings.
Handling Errors Gracefully
Always check for failures:
use std::process::Command;
fn main() {
let result = Command::new("not_a_real_command").output();
match result {
Ok(output) => println!("Success: {}", output.status),
Err(e) => eprintln!("Failed to run command: {}", e),
}
}
This avoids panicking and gives better error messages in production.
Chaining Commands
Rust does not support shell-style pipes by default. But you can simulate it:
use std::process::{Command, Stdio};
fn main() {
let grep = Command::new("grep")
.arg("rust")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("Failed to start grep");
// Add piping logic here if needed
}
You can pass input manually via stdin.write_all(…), but that is more advanced.
Supplying Input to stdin
To write to a command’s input:
use std::io::Write;
use std::process::{Command, Stdio};
fn main() {
let mut child = Command::new("grep")
.arg("Rust")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("Failed to spawn grep");
let mut stdin = child.stdin.take().expect("Failed to open stdin");
stdin.write_all(b"Rust is great\nGo is cool\n").unwrap();
let output = child.wait_with_output().expect("Failed to read output");
println!("{}", String::from_utf8_lossy(&output.stdout));
}
This allows your Rust code to simulate a pipe by writing to stdin directly.
Example: Checking Git Status
let output = Command::new("git")
.arg("status")
.output()
.expect("Failed to run git");
if output.status.success() {
let text = String::from_utf8_lossy(&output.stdout);
println!("{}", text);
} else {
eprintln!("Git command failed");
}
You can build wrappers for system tools like this, useful for automation or scripting.
Security Consideration
Avoid blindly executing user input. Never do this:
let command = "rm"; // user input
Command::new(command).status();
Always validate or sanitize commands before execution to avoid shell injection issues.
Summary
Rust’s std::process::Command makes it easy to execute external commands safely and flexibly. You can pass arguments, capture output, handle errors, and integrate CLI tools directly into your Rust application. It is perfect for scripting, automation, and building robust system utilities.
Rust Intermediate Concepts
- Generic Types in Functions and Structs
- Implementing Traits for Custom Behavior in Rust
- Trait Bounds and Constraints in Rust
- Lifetimes in Rust
- Using Closures (Anonymous Functions) in Rust
- The Iterator Trait and .next() in Rust
- Higher Order Iterator Methods: map, filter, and fold in Rust
- Using impl Trait for Simplicity in Rust
- Advanced Collections: HashSet and BTreeMap in Rust
- Custom Error Types and the Error Trait in Rust
- Option and Result Combinators in Rust
- Writing Unit Tests in Rust
- Integration Testing in Rust
- Logging in Rust with the log Crate
- Cargo Tips and Tricks for Rust Projects
- CLI Argument Parsing with Clap in Rust
- File I/O and File System Operations in Rust
- Running External Commands in Rust
- Make HTTP Requests in Rust with Reqwest
- JSON Serialization and Deserialization in Rust with Serde
- Building a Weather CLI in Rust
- Date and Time Handling in Rust with Chrono
- Using Regular Expressions in Rust with the regex Crate
- Memory Management in Rust
- Understanding Borrow Checker Errors in Rust
- Interacting with Databases in Rust
- Building a Todo List CLI in Rust with SQLite
- Concurrency in Rust