External Command

Locate Installed Executables with which

which which-crates.io which-github which-lib.rs cat-filesystem cat-os

which is a Rust equivalent of Unix command "which". It locates installed executables in a cross-platform way.

//! Demonstrates how to use the `which` crate to find the location of
//! executables.

use std::env;

use which::which;

fn main() {
    // Print the current PATH environment variable.
    println!("PATH = {:?}", env::var_os("PATH"));

    // Try to find the location of common executables.
    match which("rustc") {
        Ok(path) => println!("Found rustc at: {}", path.display()),
        Err(e) => println!("Could not find rustc: {}", e),
    }

    // Try to find a command that likely doesn't exist.
    match which("some-command-that-probably-doesnt-exist") {
        Ok(path) => println!("Found command at: {}", path.display()),
        Err(e) => println!("Could not find command: {}", e),
    }
}

Run an External Command and Process its stdout

std regex cat-os cat-text-processing

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

//! This example demonstrates how to run an external command, capture its
//! output, and parse it.
use std::process::Command;

use anyhow::Result;
use anyhow::bail;
use regex::Regex;

/// Represents a Git commit with its hash and message.
#[derive(PartialEq, Default, Clone, Debug)]
struct Commit {
    hash: String,
    message: String,
}

/// Executes the `git log --oneline` command, parses its output, and prints the
/// first 5 commits.
///
/// # Returns
///
/// Returns `Ok(())` if the command executes successfully and the output is
/// parsed correctly. Returns an error if the command fails or the output cannot
/// be parsed.
fn main() -> Result<()> {
    let output = Command::new("git").arg("log").arg("--oneline").output()?;
    if !output.status.success() {
        bail!("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 an External command, Passing it stdin, then Check for an Error Code

std cat-os

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

//! This example demonstrates how to send input to an external command's
//! standard input (stdin) and process its standard output (stdout) and standard
//! error (stderr).
//!
//! It uses the `rev` command, which reverses each line of its input.
//!
//! The example sends the string "1234 56789" to `rev` and then checks if the
//! output is successful. If successful, it parses the output, splits it into
//! words, converts them to lowercase, and stores them in a HashSet to get
//! unique words.

use std::collections::HashSet;
use std::io::Write;
use std::process::Command;
use std::process::Stdio;

use anyhow::Result;
use anyhow::anyhow;
use anyhow::bail;

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

    child
        .stdin
        .as_mut()
        .ok_or(anyhow!("Child process' stdin has not been captured!"))?
        .write_all(b"1234 56789")?;

    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);
    } else {
        let err = String::from_utf8(output.stderr)?;
        bail!("External command failed:\n {}", err);
    }
    Ok(())
}

Run Piped External Commands

std cat-os

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.

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

#![allow(clippy::single_match)]
#![cfg(target_family = "unix")]

use std::process::Command;
use std::process::Stdio;

/// This example demonstrates how to pipe the output of one external command to
/// the input of another.
///
/// It uses the `du`, `sort`, and `head` commands to find the top 10 largest
/// files and directories in the current directory.
///
/// The `du` command is used to get the disk usage of each file and directory.
/// The `sort` command is used to sort the output of `du` in reverse
/// human-readable order. The `head` command is used to get the first 10 lines
/// of the sorted output.
fn main() -> anyhow::Result<()> {
    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()?;

        match sort_output_child.stdout.take() {
            Some(sort_output) => {
                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(())
}

Redirect both the stdout and stderr of a Child Process to the Same File

std cat-os

Spawns a child process and redirects std::io::Stdout⮳ and std::io::Stderr⮳ to the same file. It follows the same idea as run piped external commands, however std::process::Stdio⮳ writes to a specified file. std::fs::File::try_clone⮳ references the same file handle for std::io::Stdout⮳ and std::io::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 . oops >out.txt 2>&1.

#![cfg(target_family = "unix")]
use std::fs;
use std::fs::File;
use std::path::Path;
use std::process::Command;
use std::process::Stdio;

/// This example demonstrates how to capture both standard output and standard
/// error from an external command and redirect them to the same file.
///
/// It uses `ls` command with a non-existent directory "oops" to generate an
/// error. Both the standard output (listing of the current directory) and the
/// standard error (error message about "oops") will be written to
/// "temp/out.txt".
fn main() -> Result<(), std::io::Error> {
    let path = Path::new("temp/out.txt");
    let outputs = File::create(path)?;
    // Creates a new `File` instance that shares the same underlying file handle
    // as the existing File instance. Reads, writes, and seeks will affect both
    // `File` instances simultaneously.
    let errors = outputs.try_clone()?;

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

    // Print contents of the file.
    std::fs::read_to_string(path)?
        .lines()
        .for_each(|line| println!("{}", line));

    Ok(())
}

Continuously Process the Outputs of a Child Process

std cat-os

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

The below recipe is equivalent to the Unix shell command journalctl | grep usb.

//! This example demonstrates how to continuously read from the standard output
//! of an external command, filter the output, and print the filtered lines. It
//! uses the `journalctl` command to read system logs, filters lines containing
//! "usb", and prints them to the console.
use std::io::BufRead;
use std::io::BufReader;
use std::io::Error;
use std::io::ErrorKind;
use std::process::Command;
use std::process::Stdio;

fn main() -> Result<(), Error> {
    // NOTE: `systemd` should be installed for this example to work.
    let stdout = Command::new("journalctl")
        .stdout(Stdio::piped())
        // Spawn the command and capture its standard output.
        .spawn()?
        .stdout
        // Ensure that standard output was captured successfully.
        .ok_or_else(|| {
            Error::new(ErrorKind::Other, "Could not capture standard output.")
        })?;

    // Create a buffered reader for efficient line-by-line reading.
    let reader = BufReader::new(stdout);

    // Iterate over the lines from the reader.
    reader
        .lines()
        // Convert each line to a String, discarding errors.
        .map_while(Result::ok)
        // Filter lines that contain the substring "usb".
        .filter(|line| line.contains("usb"))
        .for_each(|line| println!("{}", line));

    Ok(())
}

Read an Environment Variable

std cat-os

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

//! This example demonstrates how to read an environment variable.
//! If the environment variable is not set, a default value is used.

use std::env;

/// Reads the `PATH` environment variable, or a default value if the variable is
/// not set.
fn main() {
    let path = env::var("PATH").unwrap_or("".to_string());
    println!("Path: {}", path);
}

Run Child Processes Using duct

duct duct-crates.io duct-github duct-lib.rs

duct⮳ is a library for running child processes. duct makes it easy to build pipelines and redirect I/O like a shell. At the same time, duct helps you write correct, portable code: whitespace is never significant, errors from child processes get reported by default, and a variety of gotchas, bugs, and platform inconsistencies⮳ are handled for you.