External Command
Run an external command and process stdout
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
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
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
.
Command
s 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
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
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
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