use async_process::{Command, ExitStatus};
use std::borrow::Cow;
use std::ffi::OsStr;
#[derive(Debug, Clone, PartialEq)]
pub struct Output {
pub stdout: String,
pub stderr: String,
pub status: ExitStatus,
}
impl From<async_process::Output> for Output {
fn from(output: async_process::Output) -> Self {
Self {
stdout: String::from_utf8_lossy(&output.stdout).into(),
stderr: String::from_utf8_lossy(&output.stderr).into(),
status: output.status,
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error(
"`{}` failed with code {}:\n\n--- Stdout:\n {}\n--- Stderr:\n {}",
command,
output.status.code().unwrap_or(1),
output.stdout,
output.stderr
)]
Failed {
command: String,
output: Output,
},
}
fn display_command(cmd: &Command) -> String {
fn quote(value: &OsStr) -> String {
let value = value.to_string_lossy();
shlex::try_quote(&value).map_or_else(|_| value.to_string(), Cow::into_owned)
}
let command: Vec<String> = std::iter::once(quote(cmd.get_program()))
.chain(cmd.get_args().map(quote))
.collect();
let command = command.join(" ");
match cmd.get_current_dir() {
Some(dir) => format!("cd {} && {command}", quote(dir.as_os_str())),
None => command,
}
}
pub fn check_exit_status(cmd: &Command, output: &async_process::Output) -> Result<(), Error> {
if output.status.success() {
Ok(())
} else {
Err(Error::Failed {
command: display_command(cmd),
output: output.clone().into(),
})
}
}
pub async fn run_command(cmd: &mut Command) -> Result<Output, Error> {
let output = cmd.output().await?;
check_exit_status(cmd, &output)?;
Ok(output.into())
}
#[cfg(test)]
mod tests {
use similar_asserts::assert_eq as sim_assert_eq;
#[test]
fn display_command_omits_the_environment() {
let mut cmd = async_process::Command::new("sh");
cmd.args(["-c", "cargo update --offline"]);
cmd.env("API_TOKEN", "s3cr3t");
cmd.current_dir("/home/user/my repo");
let rendered = super::display_command(&cmd);
sim_assert_eq!(
rendered,
"cd '/home/user/my repo' && sh -c 'cargo update --offline'"
);
}
}