Executing Commands in Rust: How to Capture Output in 2025

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

From Python to Rust: The Evolution of Command Execution

\n\n

If you've ever wondered "How do I invoke a system command and capture its output?" on Stack Overflow, you know: this is one of the most popular and vital questions for any developer. In the Python world, the answer is simple — subprocess.check_output(). But what do you do if you've switched to Rust and want not just to run a command, but to do it elegantly, safely, and with maximum performance?

\n\n

Rust is not just "another language." It's a system where every byte is under control, and command execution errors don't crash the entire application. In 2025, when security and speed have become the de facto standard, the ability to work correctly with subprocesses in Rust is a must-have skill. Let's break down the best practices that will turn running system commands from a chore into an art.

\n\n

std::process::Command: Your Swiss Army Knife

\n\n

The heart of command execution in Rust is the std::process::Command module. It provides everything you need to launch external programs, capture their output, and handle errors. Unlike Python, where subprocess hides many details, Rust gives you full control.

\n\n

Basic Example: Capturing stdout

\n\n

Let's start with a classic — execute the echo "Hello, Rust!" command and capture its output:

\n\n
use std::process::Command;\n\nfn main() -> Result<(), Box<dyn std::error::Error>> {\n    let output = Command::new("echo")\n        .arg("Hello, Rust!")\n        .output()?;\n\n    let stdout = String::from_utf8(output.stdout)?;\n    println!("Command output: {}", stdout);\n\n    Ok(())\n}\n
\n\n

What's important here? We use .output(), which returns an Output structure. It contains three fields: stdout, stderr, and status. Note the ? — this is the error propagation operator, which saves us from cumbersome match blocks. Rust forces us to explicitly handle possible failures: the command might not exist, lack permissions, or simply exit with an error.

\n\n

Handling stderr and Return Code

\n\n

In real-world scenarios, you need not only stdout but also stderr. Imagine you run ls /nonexistent — a command that will guaranteed to produce an error:

\n\n
use std::process::Command;\n\nfn main() {\n    let output = Command::new("ls")\n        .arg("/nonexistent")\n        .output()\n        .expect("Failed to execute command");\n\n    if output.status.success() {\n        let stdout = String::from_utf8_lossy(&output.stdout);\n        println!("Success: {}", stdout);\n    } else {\n        let stderr = String::from_utf8_lossy(&output.stderr);\n        eprintln!("Error (code {}): {}", output.status.code().unwrap_or(-1), stderr);\n    }\n}\n
\n\n

Here we use String::from_utf8_lossy() — a safe way to convert bytes to a string, which replaces invalid UTF-8 characters with �. This is critical if the command output contains binary data or uses an unpredictable encoding.

\n\n

Advanced Techniques: Asynchrony and Pipes

\n\n

In 2025, synchronous command execution is a faux pas. Modern applications require asynchrony, especially if you're working with web servers or processing multiple commands simultaneously. Rust, with its Tokio ecosystem, provides an elegant solution.

\n\n

Asynchronous Output Capture with Tokio

\n\n

Add the dependency to Cargo.toml:

\n\n
[dependencies]\ntokio = { version = "1", features = ["full"] }\n
\n\n

Now let's execute the command asynchronously:

\n\n
use tokio::process::Command;\n\n#[tokio::main]\nasync fn main() -> Result<(), Box<dyn std::error::Error>> {\n    let output = Command::new("sleep")\n        .arg("2")\n        .output()\n                

Blogs

Book Recommendations