Question

Run System Commands and Capture Output in Rust

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.

Take Quiz

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 both stdout and stderr
  • output.stdout and output.stderr are 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:

  1. Command::new("echo")

    • Rust prepares to run the external program named echo.
  2. .arg("hello")

    • One argument is added, so the final command is effectively echo hello.
  3. .output()

    • Rust starts the program.
    • Rust waits until it finishes.
    • Rust collects:
      • the exit status
      • standard output
      • standard error
  4. .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.
  5. String::from_utf8_lossy(&output.stdout)

    • The captured bytes from stdout are converted into readable text.
  6. 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, or kubectl and process the result.
  • System administration scripts

    • Run commands like ls, ps, df, or fuser and inspect their output.
  • 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.

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.

Take Quiz

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

  • ls is common on Unix-like systems
  • Windows typically uses different commands

If you need portability, choose commands and paths carefully.

Comparisons

ApproachWhat it doesCaptures output?Best use
Command::new(...).status()Runs a command and returns only the exit statusNoWhen you only care whether it succeeded
Command::new(...).output()Runs a command and captures stdout, stderr, and statusYesWhen you need to read command output
Command::new(...).spawn()Starts a command without waiting immediatelyNot by defaultLong-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

StyleExampleSafer?Notes
Direct executionCommand::new("ls").arg("-l")YesPreferred for most cases
Shell execution`Command::new("sh").args(["-c", "ls -lgrep txt"])`No
Take Quiz

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() captures stdout, 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.

Take Quiz

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

  1. Run an external command using std::process::Command.
  2. Pass at least one argument to the command.
  3. Capture both stdout and stderr.
  4. Print whether the command succeeded.
  5. Show the captured output in a readable text form.
Take Quiz

Keep learning

Related questions

View more