From Python to Rust: The Evolution of Command Execution
\n\nIf 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?
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\nstd::process::Command: Your Swiss Army Knife
\n\nThe 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.
Basic Example: Capturing stdout
\n\nLet's start with a classic — execute the echo "Hello, Rust!" command and capture its output:
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\nWhat'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.
Handling stderr and Return Code
\n\nIn 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:
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\nHere 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.
Advanced Techniques: Asynchrony and Pipes
\n\nIn 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\nAsynchronous Output Capture with Tokio
\n\nAdd the dependency to Cargo.toml:
[dependencies]\ntokio = { version = "1", features = ["full"] }\n\n\nNow let's execute the command asynchronously:
\n\nuse 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