Выполняем команды в Rust: как захватить вывод в 2025

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

От Python к Rust: эволюция выполнения команд

Если вы когда-либо задавались вопросом «How do I invoke a system command and capture its output?» на Stack Overflow, то знаете: это один из самых популярных и жизненно важных вопросов для любого разработчика. В мире Python ответ прост — subprocess.check_output(). Но что делать, если вы перешли на Rust и хотите не просто запустить команду, а сделать это элегантно, безопасно и с максимальной производительностью?

Rust — это не просто «ещё один язык». Это система, где каждый байт под контролем, а ошибки выполнения команд не приводят к краху всего приложения. В 2025 году, когда безопасность и скорость стали стандартом де-факто, умение правильно работать с подпроцессами в Rust — must-have навык. Давайте разберём лучшие практики, которые превратят запуск системных команд из рутины в искусство.

std::process::Command: ваш швейцарский нож

Сердце выполнения команд в Rust — модуль std::process::Command. Он предоставляет всё необходимое для запуска внешних программ, захвата их вывода и обработки ошибок. В отличие от Python, где subprocess скрывает многие детали, Rust даёт вам полный контроль.

Базовый пример: захват stdout

Начнём с классики — выполним команду echo "Hello, Rust!" и захватим её вывод:

use std::process::Command;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let output = Command::new("echo")
        .arg("Hello, Rust!")
        .output()?;

    let stdout = String::from_utf8(output.stdout)?;
    println!("Вывод команды: {}", stdout);

    Ok(())
}

Что здесь важно? Мы используем .output(), который возвращает структуру Output. Она содержит три поля: stdout, stderr и status. Обратите внимание на ? — это оператор распространения ошибок, который избавляет нас от громоздких match блоков. Rust заставляет нас явно обрабатывать возможные сбои: команда может не существовать, не иметь прав доступа или просто завершиться с ошибкой.

Обработка stderr и кода возврата

В реальных сценариях вам нужно не только stdout, но и stderr. Представьте, что вы запускаете ls /nonexistent — команда, которая гарантированно выдаст ошибку:

use std::process::Command;

fn main() {
    let output = Command::new("ls")
        .arg("/nonexistent")
        .output()
        .expect("Не удалось выполнить команду");

    if output.status.success() {
        let stdout = String::from_utf8_lossy(&output.stdout);
        println!("Успех: {}", stdout);
    } else {
        let stderr = String::from_utf8_lossy(&output.stderr);
        eprintln!("Ошибка (код {}): {}", output.status.code().unwrap_or(-1), stderr);
    }
}

Здесь мы используем String::from_utf8_lossy() — безопасный способ преобразования байтов в строку, который заменяет невалидные UTF-8 символы на �. Это критично, если вывод команды содержит бинарные данные или использует непредсказуемую кодировку.

Продвинутые техники: асинхронность и пайпы

В 2025 году синхронное выполнение команд — это моветон. Современные приложения требуют асинхронности, особенно если вы работаете с веб-серверами или обрабатываете множество команд одновременно. Rust с его экосистемой Tokio предоставляет элегантное решение.

Асинхронный захват вывода с Tokio

Добавим в Cargo.toml зависимость:

[dependencies]
tokio = { version = "1", features = ["full"] }

Теперь выполним команду асинхронно:

use tokio::process::Command;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let output = Command::new("sleep")
        .arg("2")
        .output()
        .await?;

    println!("Команда завершена через 2 секунды");
    Ok(())
}

Асинхронный Command работает почти так же, как синхронный, но не блокирует поток выполнения. Это позволяет вашему приложению продолжать обрабатывать другие задачи, пока команда выполняется. Особенно полезно при запуске долгих операций, например, ffmpeg или curl.

Работа с пайпами: передача данных между командами

Иногда нужно не просто выполнить команду, а соединить их в цепочку, как в bash: ps aux | grep rust. В Rust это делается через перенаправление ввода-вывода:

use std::process::{Command, Stdio};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Запускаем ps aux, захватываем его stdout
    let ps = Command::new("ps")
        .arg("aux")
        .stdout(Stdio::piped())
        .spawn()?;

    // Передаём stdout ps как stdin для grep
    let grep = Command::new("grep")
        .arg("rust")
        .stdin(ps.stdout.unwrap())
        .output()?;

    let result = String::from_utf8_lossy(&grep.stdout);
    println!("Процессы Rust:\n{}", result);

    Ok(())
}

Обратите внимание: мы используем .spawn() вместо .output() для первой команды. spawn() запускает процесс, не дожидаясь его завершения, и возвращает handle, из которого можно забрать stdout. Это эффективно, так как данные передаются через pipe без промежуточного буферизирования.

Безопасность и лучшие практики

Выполнение системных команд — это всегда риск. В Rust есть несколько механизмов, которые помогают писать безопасный код.

Избегайте shell injection

В Python многие пишут os.system("ls " + user_input) — это прямой путь к катастрофе. В Rust такой проблемы нет, потому что Command не использует shell. Но это не значит, что можно расслабиться:

// Плохо: передача пользовательского ввода как аргумента
let filename = user_input; // может быть "; rm -rf /"
let output = Command::new("cat").arg(filename).output()?;

// Хорошо: валидация ввода
let filename = sanitize_filename(&user_input);
let output = Command::new("cat").arg(filename).output()?;

Всегда проверяйте и санируйте ввод, особенно если команда выполняется с повышенными привилегиями. Используйте библиотеки вроде sanitize-filename для дополнительной защиты.

Управление временем выполнения

Что если команда зависла навсегда? В Rust можно установить тайм-аут:

use std::process::Command;
use std::time::Duration;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut child = Command::new("sleep")
        .arg("10")
        .spawn()?;

    // Ждём не                

Блоги

Рекомендаций по книгам