I usually use the Command
's Debug
representation because it'll show you the original command and all its arguments. Everything has loads of quotes, but something like whatever.with_context(|| format!("Command failed with exit code {}: {:?}", exit_code, cmd)
works pretty well.
Ewww.... I guess you could buffer the entire stderr
stream and wait til the end to see whether you need to catch it? but that's a pretty poor user experience (e.g. users won't see progress for long running commands until the end), and unless you're redirecting it to a temporary file you may have issues with filling up pipes.
Spawning threads is equally as messy because you've added a concurrency element. Although I guess you could skip avoid that by Command::spawn()
ing the child process in the background and having your main thread continually read stderr
or stdout
in a loop?
If it helps, I've got a small program which automates the build/release process for a thing at work and this is the code related to executing commands:
// src/utils.rs
use anyhow::Error;
use std::process::{Command, ExitStatus};
/// Execute a command.
pub fn execute(mut cmd: Command, env: &Environment) -> Result<(), Error> {
log::debug!("Executing {:?}", cmd);
if !env.dry_run {
let exit_status = cmd
.status()
.with_context(|| format!("Unable to invoke {:?}", cmd))?;
check_exit_status(&cmd, exit_status)?;
}
Ok(())
}
pub fn check_exit_status(
cmd: &Command,
exit_status: ExitStatus,
) -> Result<(), Error> {
if exit_status.success() {
Ok(())
} else {
Err(Error::from(ExecutionError {
command: format!("{:?}", cmd),
exit_code: exit_status.code(),
}))
}
}
/// Execution of a [`Command`] finished with an unsuccessful error code.
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
#[error("Received an exit code of {} from {}", exit_code.unwrap_or(1), command)]
pub struct ExecutionError {
pub command: String,
pub exit_code: Option<i32>,
}
It's a lot simpler in that we pass stderr
and stdout
through to the attached TTY so users can see the output of each build step as they happen.