External Command

Run an external command and process stdout

regex-badge cat-os-badge cat-text-processing-badge

Add the regex crate to your own project:

cargo add regex

Runs git log --oneline as an external Command and inspects its Output using Regex to get the hash and message of the last 5 commits.

use std::error::Error;
use std::process::Command;
use regex::Regex;

#[derive(PartialEq, Default, Clone, Debug)]
struct Commit {
    hash: String,
    message: String,
}

fn main() -> Result<(), Box<dyn Error>> {
    let output = Command::new("git").arg("log").arg("--oneline").output()?;

    if !output.status.success() {
        return Err(Box::<dyn Error>::from("Command executed with failing error code"));
    }

    let pattern = Regex::new(r"(?x)
                               ([0-9a-fA-F]+) # commit hash
                               (.*)           # The commit message")?;

    String::from_utf8(output.stdout)?
        .lines()
        .filter_map(|line| pattern.captures(line))
        .map(|cap| {
                 Commit {
                     hash: cap[1].to_string(),
                     message: cap[2].trim().to_string(),
                 }
             })
        .take(5)
        .for_each(|x| println!("{:?}", x));

    Ok(())
}

Run the example inside a git repository:

cargo run --example process_output

Run an external command passing it stdin and check for an error code

std-badge cat-os-badge

This example requires a python3 interpreter installed.

Opens the python interpreter using an external Command and passes it a python statement for execution. Output of statement is then parsed.

use std::collections::HashSet;
use std::io::Write;
use std::process::{Command, Stdio};
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    let mut child = Command::new("python3").stdin(Stdio::piped())
        .stderr(Stdio::piped())
        .stdout(Stdio::piped())
        .spawn()?;

    child.stdin
        .as_mut()
        .ok_or("Child process stdin has not been captured!")?
        .write_all(b"copyright(); exit()")?;

    let output = child.wait_with_output()?;

    if output.status.success() {
        let raw_output = String::from_utf8(output.stdout)?;
        let words = raw_output.split_whitespace()
            .map(|s| s.to_lowercase())
            .collect::<HashSet<_>>();
        println!("Found {} unique words:", words.len());
        println!("{:#?}", words);
        Ok(())
    } else {
        let err = String::from_utf8(output.stderr)?;
        return Err(Box::<dyn Error>::from(err));
    }
}

Run this example from the cookbook source code directory:

cargo run --example command_input_output

Run piped external commands

std-badge cat-os-badge

Shows up to the 10th biggest files and subdirectories in the current working directory. It is equivalent to running: du -ah . | sort -hr | head -n 10.

Commands represent a process. Output of a child process is captured with a Stdio::piped between parent and child.

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

fn main() -> Result<(), Box<dyn Error>> {
    let directory = std::env::current_dir()?;
    let mut du_output_child = Command::new("du")
        .arg("-ah")
        .arg(&directory)
        .stdout(Stdio::piped())
        .spawn()?;

    if let Some(du_output) = du_output_child.stdout.take() {
        let mut sort_output_child = Command::new("sort")
            .arg("-hr")
            .stdin(du_output)
            .stdout(Stdio::piped())
            .spawn()?;

        du_output_child.wait()?;

        if let Some(sort_output) = sort_output_child.stdout.take() {
            let head_output_child = Command::new("head")
                .args(&["-n", "10"])
                .stdin(sort_output)
                .stdout(Stdio::piped())
                .spawn()?;

            let head_stdout = head_output_child.wait_with_output()?;

            sort_output_child.wait()?;

            println!(
                "Top 10 biggest files and directories in '{}':\n{}",
                directory.display(),
                String::from_utf8(head_stdout.stdout).unwrap()
            );
        }
    }

    Ok(())
}

To run the example from the cookbook source code:

cargo run --example pipelined_commands

Redirect both stdout and stderr of child process to the same file

std-badge cat-os-badge

Spawns a child process and redirects stdout and stderr to the same file. It follows the same idea as run piped external commands, however process::Stdio writes to a specified file. File::try_clone references the same file handle for stdout and stderr. It will ensure that both handles write with the same cursor position.

The below recipe is equivalent to run the Unix shell command ls /tmp /tmp/oops > /tmp/out.txt 2>&1.

Since the /tmp/oops file doesn't exist (unless you create it!), the ls command will generate an error on stderr as well as the normal output on stdout.

use std::fs::File;
use std::io::Error;
use std::process::{Command, Stdio};

fn main() -> Result<(), Error> {
    let outputs = File::create("/tmp/out.txt")?;
    let errors = outputs.try_clone()?;

    Command::new("ls")
        .args(&["/tmp", "/tmp/oops"])
        .stdout(Stdio::from(outputs))
        .stderr(Stdio::from(errors))
        .spawn()?
        .wait_with_output()?;

    Ok(())
}

To run this from the cookbook source code directory:

cargo run --example command_stderr

Examine the generated output:

cat /tmp/out.txt

Continuously process child process' outputs

std-badge cat-os-badge

In Run an external command and process stdout, processing doesn't start until external Command is finished. The recipe below calls Stdio::piped to create a pipe, and reads stdout continuously as soon as the BufReader is updated.

The below recipe is equivalent to the Unix shell command lspci | grep bridge.

use std::process::{Command, Stdio};
use std::io::{BufRead, BufReader, Error, ErrorKind};

fn main() -> Result<(), Error> {
    let stdout = Command::new("lsusb")
        .stdout(Stdio::piped())
        .spawn()?
        .stdout
        .ok_or_else(|| Error::new(ErrorKind::Other,"Could not capture standard output."))?;

    let reader = BufReader::new(stdout);

    reader
        .lines()
        .filter_map(|line| line.ok())
        .filter(|line| line.find("bridge").is_some())
        .for_each(|line| println!("{}", line));

     Ok(())
}

Run this example from the cookbook source code directory:

cargo run --example command_read_stdout

Read Environment Variable

std-badge cat-os-badge

Reads an environment variable via std::env::var.

use std::env;
use std::fs;
use std::io::Error;

fn main() -> Result<(), Error> {

    // Create the contents of the default config file
    fs::write("/tmp/config-xyz", b"hello there!")?;

    // read `config_path` from the environment variable `CONFIG`.
    // If `CONFIG` isn't set, fall back to a default config path.
    let config_path = env::var("CONFIG_XYZ")
        .unwrap_or("/tmp/config-xyz".to_string());

    let config: String = fs::read_to_string(config_path)?;
    println!("Config file contents: {}", config);

    Ok(())
}

Run this example from the cookbook source code directory:

cargo run --example read_env_variable

Try creating a CONFIG environment variable with the filename of a file that exists:

export CONFIG="/tmp/my_config"
echo "hello there" > /tmp/my_config
cargo run --example read_env_variable