Avatar ·

Как выполнить системную команду и захватить её вывод в Rust?

Привет, коллеги! Я начал изучать Rust и пишу небольшой инструмент для автоматизации. Мне нужно выполнить внешнюю системную команду (например, `ls -la` или `git status`) и прочитать её вывод (stdout и stderr) прямо в программе, чтобы потом обработать строки. Я посмотрел документацию по модулю `std::process::Command`, но у меня не получается правильно захватить вывод. Вот что я пробовал: ```rust use std::process::Command; fn main() { let output = Command::new("ls") .arg("-la") .output() .expect("Не удалось выполнить команду"); println!("stdout: {}", output.stdout); println!("stderr: {}", output.stderr); } ``` Но в консоль выводится что-то вроде `stdout: [115, 111, 109, 101, 32, 116, 101, 120, 116]` — это же байты, а не строка! Как мне получить читаемый текст? И что делать, если команда может завершиться с ошибкой? Хочу понять, как правильно обрабатывать статус выхода и как захватывать stderr отдельно. Заранее спасибо за помощь! P.S. Если можно, пример с обработкой ошибок и с использованием `unwrap` / `expect` — чтобы было понятно новичку. <<>>

Правильный способ захвата вывода команды с преобразованием в строку

Вы всё делаете почти правильно, но забыли один важный шаг: output.stdout — это Vec<u8> (байтовый вектор), а не строка. Чтобы получить текст, нужно преобразовать байты в строку с помощью String::from_utf8_lossy() (или from_utf8() для строгой проверки).

Вот исправленный пример с полной обработкой:

use std::process::Command;

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

    // Проверяем статус выхода
    if result.status.success() {
        // Преобразуем stdout в строку (lossy — заменяет невалидный UTF-8 на �)
        let stdout = String::from_utf8_lossy(&result.stdout);
        println!("Вывод команды:\n{}", stdout);
    } else {
        // Если ошибка — выводим stderr
        let stderr = String::from_utf8_lossy(&result.stderr);
        eprintln!("Команда завершилась с ошибкой: {}", result.status);
        eprintln!("stderr:\n{}", stderr);
    }
}

Что здесь происходит?

  • Command::new("ls") — создаёт команду.
  • .arg("-la") — добавляет аргумент.
  • .output() — запускает команду и ждёт её завершения, возвращая Output (содержит stdout, stderr и status).
  • result.status.success() — проверяет, завершилась ли команда с кодом 0.
  • String::from_utf8_lossy() — безопасно преобразует байты в строку, заменяя невалидные последовательности на символ замены.

Более продвинутый пример с обработкой ошибок через Result

Если хотите избежать паники при ошибке запуска команды:

use std::process::Command;

fn run_command(cmd: &str, args: &[&str]) -> Result<String, String> {
    let output = Command::new(cmd)
        .args(args)
        .output()
        .map_err(|e| format!("Ошибка запуска '{}': {}", cmd, e))?;

    if output.status.success() {
        Ok(String::from_utf8_lossy(&output.stdout).to_string())
    } else {
        let stderr = String::from_utf8_lossy(&output.stderr);
        Err(format!("Команда '{}' завершилась с кодом {}: {}", 
                     cmd, output.status, stderr))
    }
}

fn main() {
    match run_command("git", &["status"]) {
        Ok(output) => println!("Git status:\n{}", output),
        Err(e) => eprintln!("Ошибка: {}", e),
    }
}

Важные замечания

  • Не используйте unwrap() в production-коде — лучше expect() с осмысленным сообщением или обработка через match.
  • Для длительного вывода (например, логи) используйте .stdout(Stdio::piped()) и читайте построчно через BufReader.
  • Кодировка: from_utf8_lossy подходит для большинства систем (Linux/macOS обычно используют UTF-8). Для Windows может потребоваться перекодировка из UTF-16.
  • Безопасность: избегайте передачи пользовательского ввода напрямую в Command::new() — используйте .arg() для каждого аргумента, чтобы избежать shell-инъекций.
<<>>

Альтернатива: захват вывода с помощью оболочки (shell)

Иногда удобнее выполнить команду через оболочку, особенно если нужны пайпы (конвейеры) или подстановки. Для этого в Rust можно использовать sh -c:

use std::process::Command;

fn main() {
    let output = Command::new("sh")
        .arg("-c")
        .arg("ls -la | grep '^d' | wc -l")  // считаем количество директорий
        .output()
        .expect("Ошибка выполнения");

    let stdout = String::from_utf8_lossy(&output.stdout);
    println!("Количество директорий: {}", stdout.trim());
}

Важные предостережения

  • Не используйте этот подход с пользовательским вводом — это риск shell-инъекций. Если нужно передать аргументы от пользователя, всегда используйте .arg() без оболочки.
  • Кроссплатформенность: на Windows нужно использовать cmd /C вместо sh -c. Для кроссплатформенных проектов лучше избегать shell-команд или писать условную компиляцию.
  • Производительность: запуск оболочки добавляет накладные расходы. Для простых команд с одним процессом лучше использовать прямой вызов Command::new().

Этот подход удобен для быстрых скриптов или прототипирования, но в серьёзных проектах старайтесь обходиться без оболочки.