Rust: Executing external commands | Glenn Gillen
Rust: Executing external commands

Rust: Executing external commands

Mar 23, 2022

As with most languages, there's multiple ways to execute or shell out to a process. The most straight-forward (IMO) is to create a new Command and call output():

use std::process::Command;
fn main() {
let output = Command::new("ls")
.arg("-l")
.arg("-a")
.output()
.expect("failed to execute process");
println!("status: {}", output.status);
println!("stdout: {}", String::from_utf8_lossy(&output.stdout));
println!("stderr: {}", String::from_utf8_lossy(&output.stderr));
}

As we've discussed in the earlier editions, Rust gives you the tooling to code defensively and handle error cases properly. Or it'll let you be super lazy for the sake of demoing how something works. The latest version of being lazy for us is using expect(). Think of it as the compliment to unwrap(). Where unwrap() took a Result and effectively said "let's just assume everything worked, I only want the output of the success" what expect() is saying is "if something goes wrong, raise an error of 'failed to execute process'... but really I still only just want the output". The result of output() is a Result, and you should handle it (properly) as such.

But for now, we're taking a shortcut to show what you can do with the succesful result which we've assigned to output. As you'll see from the println!() statements it gives us access to the exit code (status) as well as stdout and stderr.If all you need is the exit code there is actually an alternative method to call in status().

For a little control of things you can instead call spawn() and get a reference to the child process:

use std::process::{Command, Stdio};
use std::io::{BufRead, BufReader, Error, ErrorKind};
fn main() -> Result<(), Error> {
let stdout = Command::new("ls")
.arg("-l")
.arg("-a")
.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(" .").is_some())
.for_each(|line| println!("{}", line));
Ok(())
}

Some more new Rust additions in this approach! We've updated main() to be able to return a result. We've passed in a new pipe Stdio::piped() into our command so we can connect to the child process. We're using the ? operator on the call to spawn() to give us just the success result. We're then accessing stdout property and calling ok_or_else() to have it throw an Error if we don't have access to stdout for some reasons. Whew! That's a lot... in surprisingly little code all things considered.

Next... we're putting our new stdout pipe into a buffer reader, and then we read. Filtering for lines with values, then filtering further for lines looking for any that contain ., where is_some() is returning true where the result has a value (though we don't care to check exactly what the value is). Finally we loop over all of the matching lines and print them out.

A call to ls probably isn't the most valuable demonstration this though it's enough to see all of the moving parts in action. You can take the same approach with a writer feeding into stdin.

Hi, I'm Glenn! 👋 I've spent most of my career working with or at startups. I'm currently the Director of Product @ Ockam where I'm helping developers build applications and systems that are secure-by-design. It's time we started securely connecting apps, not networks.

Previously I led the Terraform product team @ HashiCorp, where we launched Terraform Cloud and set the stage for a successful IPO. Prior to that I was part of the Startup Team @ AWS, and earlier still an early employee @ Heroku. I've also invested in a couple of dozen early stage startups.