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'm currently Director of Product (Terraform) @ HashiCorp, and we're hiring! If you'd like to come and work with me and help make Terraform Cloud even more amazing we have multiple positions opening in Product ManagementDesign, and Engineering & Engineering Management across a range of levels (i.e., junior through to senior). Please send in an application ASAP so we can get in touch.