#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Success {
pub stdout: String,
pub stderr: String,
}
pub fn run<Prog, Args, Arg>(program: Prog, args: Args) -> Result<Success, Error>
where
Prog: AsRef<std::ffi::OsStr>,
Args: IntoIterator<Item = Arg>,
Arg: AsRef<std::ffi::OsStr>,
{
let mut command = std::process::Command::new(program);
command.args(args);
execute(&mut command)
}
fn execute(cmd: &mut std::process::Command) -> Result<Success, Error> {
let std::process::Output { status, stdout, stderr } = cmd.output()?;
if !status.success() {
return Err(Error::UnsuccessfulTermination { status, stdout, stderr });
}
let stdout = String::from_utf8(stdout).map_err(Error::StdoutNotUtf8)?;
let stderr = String::from_utf8(stderr).map_err(Error::StderrNotUtf8)?;
Ok(Success { stdout, stderr })
}
#[derive(Debug)]
pub enum Error {
Io(std::io::Error),
UnsuccessfulTermination {
status: std::process::ExitStatus,
stdout: Vec<u8>,
stderr: Vec<u8>,
},
StdoutNotUtf8(std::string::FromUtf8Error),
StderrNotUtf8(std::string::FromUtf8Error),
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Io(src) => Some(src),
Error::UnsuccessfulTermination { .. } => None,
Error::StdoutNotUtf8(src) => Some(src),
Error::StderrNotUtf8(src) => Some(src),
}
}
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Self {
Error::Io(err)
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Io(_) => write!(f, "IO error"),
Error::UnsuccessfulTermination { status, stdout: _, stderr } => {
let stderr = String::from_utf8_lossy(stderr);
let mut stderr_tail = stderr.lines().rev().take(10).collect::<Vec<_>>();
stderr_tail.reverse();
writeln!(f, "Unsuccessful termination with exit status {status} and stderr (last 10 lines):")?;
writeln!(f, "{}", stderr_tail.join("\n"))?;
Ok(())
}
Error::StdoutNotUtf8(_) => write!(f, "stdout is not valid UTF-8"),
Error::StderrNotUtf8(_) => write!(f, "stderr is not valid UTF-8"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::os::unix::process::ExitStatusExt;
fn generate_lines(prefix: &str, lines: usize) -> Vec<u8> {
(1..=lines).map(|i| format!("{prefix} line {}\n", i)).collect::<String>().into_bytes()
}
#[test]
fn test_unsuccessful_termination_full_stderr() {
let stderr = generate_lines("stderr", 12);
let error = Error::UnsuccessfulTermination {
status: std::process::ExitStatus::from_raw(1),
stdout: Vec::new(),
stderr: stderr.clone(),
};
let error_message = error.to_string();
let expected = indoc::indoc! {r#"
Unsuccessful termination with exit status signal: 1 (SIGHUP) and stderr (last 10 lines):
stderr line 3
stderr line 4
stderr line 5
stderr line 6
stderr line 7
stderr line 8
stderr line 9
stderr line 10
stderr line 11
stderr line 12
"#};
assert_eq!(error_message, expected);
}
#[test]
fn test_unsuccessful_termination_short_stderr() {
let stderr = generate_lines("stderr", 4);
let error = Error::UnsuccessfulTermination {
status: std::process::ExitStatus::from_raw(1),
stdout: Vec::new(),
stderr: stderr.clone(),
};
let error_message = error.to_string();
let expected = indoc::indoc! {r#"
Unsuccessful termination with exit status signal: 1 (SIGHUP) and stderr (last 10 lines):
stderr line 1
stderr line 2
stderr line 3
stderr line 4
"#};
assert_eq!(error_message, expected);
}
}