use std::ffi::OsStr;
use std::io;
use std::process::{self, Command, ExitStatus};
#[derive(Debug, thiserror::Error)]
pub enum CmdError {
#[error("command '{0}' failed to start")]
NoRun(String, #[source] io::Error),
#[error("command '{0}' exited with non-zero status code {1}")]
Unsuccessful(String, i32, #[source] Option<anyhow::Error>),
#[error("command '{0}' was terminated unexpectedly")]
Terminated(String),
}
impl CmdError {
pub fn no_run(cmd: &process::Command, error: io::Error) -> Self {
CmdError::NoRun(format!("{cmd:?}"), error)
}
pub fn status_into_result(
status: process::ExitStatus,
cmd: &process::Command,
cmd_output: impl FnOnce() -> Option<String>,
) -> Result<(), Self> {
if status.success() {
Ok(())
} else if let Some(code) = status.code() {
Err(CmdError::Unsuccessful(
format!("{cmd:?}"),
code,
cmd_output().map(anyhow::Error::msg),
))
} else {
Err(CmdError::Terminated(format!("{cmd:?}")))
}
}
}
#[derive(Debug)]
pub struct Cmd {
pub cmd: std::process::Command,
ignore_exitcode: bool,
}
impl std::ops::Deref for Cmd {
type Target = std::process::Command;
fn deref(&self) -> &Self::Target {
&self.cmd
}
}
impl std::ops::DerefMut for Cmd {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.cmd
}
}
impl From<std::process::Command> for Cmd {
fn from(cmd: std::process::Command) -> Self {
Cmd {
cmd,
ignore_exitcode: false,
}
}
}
impl From<Cmd> for std::process::Command {
fn from(cmd: Cmd) -> Self {
cmd.into_inner()
}
}
impl Cmd {
pub fn new(program: impl AsRef<OsStr>) -> Self {
Self {
cmd: Command::new(program),
ignore_exitcode: false,
}
}
pub fn ignore_exitcode(&mut self) -> &mut Self {
self.ignore_exitcode = true;
self
}
pub fn run(&mut self) -> Result<(), CmdError> {
self.cmd
.status()
.map_err(|e| CmdError::no_run(&self.cmd, e))
.and_then(|v| {
if self.ignore_exitcode {
Ok(())
} else {
CmdError::status_into_result(v, &self.cmd, || None)
}
})
}
pub fn status(&mut self) -> Result<ExitStatus, CmdError> {
self.cmd
.status()
.map_err(|e| CmdError::no_run(&self.cmd, e))
}
fn print_output(&self, output: &std::process::Output) {
use std::io::Write;
std::io::stdout().write_all(&output.stdout[..]).ok();
std::io::stderr().write_all(&output.stderr[..]).ok();
}
pub fn output<T>(
&mut self,
func: impl FnOnce(std::process::Output) -> T,
) -> Result<T, CmdError> {
match self.cmd.output() {
Err(err) => Err(CmdError::no_run(&self.cmd, err)),
Ok(result) => if self.ignore_exitcode {
self.print_output(&result);
Ok(())
} else {
CmdError::status_into_result(result.status, &self.cmd, || {
Some(
String::from_utf8_lossy(&result.stderr[..])
.trim_end()
.to_string(),
)
})
}
.map_err(|e| {
self.print_output(&result);
e
})
.map(|_| func(result)),
}
}
pub fn stdout(&mut self) -> Result<String, CmdError> {
self.output(|output| {
String::from_utf8_lossy(&output.stdout[..])
.trim_end()
.to_string()
})
}
pub fn stderr(&mut self) -> Result<String, CmdError> {
self.output(|output| {
String::from_utf8_lossy(&output.stderr[..])
.trim_end()
.to_string()
})
}
pub fn into_inner(self) -> std::process::Command {
self.cmd
}
}
#[macro_export]
macro_rules! cmd_build {
($builder:ident $(, $(@$cmdargs:expr,)* $cmdarg:expr)* $(; $($k:ident = $v:tt),*)?) => {{
$(
$($builder .args($cmdargs);)*
$builder .arg($cmdarg);
)*
$($($builder . $k $v;)*)?
$builder
}}
}
#[macro_export]
macro_rules! cmd {
($cmd:expr $(, $(@$cmdargs:expr,)* $cmdarg:expr)* $(; $($k:ident = $v:tt),*)?) => {{
let mut cmd = $crate::cmd::Cmd::new($cmd);
$crate::cmd_build!(cmd $(, $(@$cmdargs,)* $cmdarg)* $(; $($k = $v),* )?)
}};
}