Running External Commands in Rust

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.