#![deny(missing_docs)]
use std::borrow::Cow;
use std::collections::HashMap;
use std::ffi::{OsStr, OsString};
use std::io::Read;
use std::os::unix::ffi::OsStrExt;
use std::path::PathBuf;
use std::{fmt, io, process};
#[derive(Debug)]
pub enum ErrorKind {
Run(io::Error),
Exit(process::ExitStatus),
}
#[derive(Debug)]
pub struct Error {
pub command: Command,
pub kind: ErrorKind,
}
impl Error {
pub fn is_run_error(&self) -> bool {
matches!(self.kind, ErrorKind::Run(_))
}
pub fn is_exit_error(&self) -> bool {
matches!(self.kind, ErrorKind::Exit(_))
}
}
trait IntoError<T> {
fn into_run_error(self, command: &Command) -> Result<T, Error>;
}
impl<T> IntoError<T> for Result<T, io::Error> {
fn into_run_error(self, command: &Command) -> Result<T, Error> {
self.map_err(|err| Error {
command: command.clone(),
kind: ErrorKind::Run(err),
})
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match &self.kind {
ErrorKind::Run(err) => write!(
f,
"failed to run '{}': {}",
self.command.command_line_lossy(),
err
),
ErrorKind::Exit(err) => write!(
f,
"command '{}' failed: {}",
self.command.command_line_lossy(),
err
),
}
}
}
impl std::error::Error for Error {}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Output {
pub status: process::ExitStatus,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
}
impl Output {
pub fn stdout_string_lossy(&self) -> Cow<str> {
String::from_utf8_lossy(&self.stdout)
}
pub fn stderr_string_lossy(&self) -> Cow<str> {
String::from_utf8_lossy(&self.stderr)
}
}
impl From<process::Output> for Output {
fn from(o: process::Output) -> Output {
Output {
status: o.status,
stdout: o.stdout,
stderr: o.stderr,
}
}
}
fn combine_output(mut cmd: process::Command) -> Result<Output, io::Error> {
let (mut reader, writer) = os_pipe::pipe()?;
let writer_clone = writer.try_clone()?;
cmd.stdout(writer);
cmd.stderr(writer_clone);
let mut handle = cmd.spawn()?;
drop(cmd);
let mut output = Vec::new();
reader.read_to_end(&mut output)?;
let status = handle.wait()?;
Ok(Output {
stdout: output,
stderr: Vec::new(),
status,
})
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LogTo {
Stdout,
#[cfg(feature = "logging")]
Log,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Command {
pub program: PathBuf,
pub args: Vec<OsString>,
pub dir: Option<PathBuf>,
pub log_to: LogTo,
pub log_command: bool,
pub log_output_on_error: bool,
pub check: bool,
pub capture: bool,
pub combine_output: bool,
pub clear_env: bool,
pub env: HashMap<OsString, OsString>,
}
impl Command {
pub fn new<S: AsRef<OsStr>>(program: S) -> Command {
Command {
program: program.as_ref().into(),
..Default::default()
}
}
pub fn with_args<I, S1, S2>(program: S1, args: I) -> Command
where
S1: AsRef<OsStr>,
S2: AsRef<OsStr>,
I: IntoIterator<Item = S2>,
{
Command {
program: program.as_ref().into(),
args: args.into_iter().map(|arg| arg.as_ref().into()).collect(),
..Default::default()
}
}
pub fn add_arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
self.args.push(arg.as_ref().into());
self
}
pub fn add_arg_pair<S1, S2>(&mut self, arg1: S1, arg2: S2) -> &mut Self
where
S1: AsRef<OsStr>,
S2: AsRef<OsStr>,
{
self.add_arg(arg1);
self.add_arg(arg2);
self
}
pub fn add_args<I, S>(&mut self, args: I) -> &mut Self
where
S: AsRef<OsStr>,
I: IntoIterator<Item = S>,
{
for arg in args {
self.add_arg(arg);
}
self
}
pub fn enable_capture(&mut self) -> &mut Self {
self.capture = true;
self
}
pub fn combine_output(&mut self) -> &mut Self {
self.combine_output = true;
self
}
pub fn set_dir<S: AsRef<OsStr>>(&mut self, dir: S) -> &mut Self {
self.dir = Some(dir.as_ref().into());
self
}
pub fn disable_check(&mut self) -> &mut Self {
self.check = false;
self
}
pub fn run(&self) -> Result<Output, Error> {
let cmd_str = self.command_line_lossy();
if self.log_command {
match self.log_to {
LogTo::Stdout => println!("{}", cmd_str),
#[cfg(feature = "logging")]
LogTo::Log => log::info!("{}", cmd_str),
}
}
let mut cmd: process::Command = self.into();
let out = if self.capture {
if self.combine_output {
combine_output(cmd).into_run_error(self)?
} else {
cmd.output().into_run_error(self)?.into()
}
} else {
let status = cmd.status().into_run_error(self)?;
Output {
stdout: Vec::new(),
stderr: Vec::new(),
status,
}
};
if self.check && !out.status.success() {
if self.capture && self.log_output_on_error {
let mut msg =
format!("command '{}' failed: {}", cmd_str, out.status);
if self.combine_output {
msg = format!(
"{}\noutput:\n{}",
msg,
out.stdout_string_lossy()
);
} else {
msg = format!(
"{}\nstdout:\n{}\nstderr:\n{}",
msg,
out.stdout_string_lossy(),
out.stderr_string_lossy()
);
}
match self.log_to {
LogTo::Stdout => println!("{}", msg),
#[cfg(feature = "logging")]
LogTo::Log => log::error!("{}", msg),
}
}
return Err(Error {
command: self.clone(),
kind: ErrorKind::Exit(out.status),
});
}
Ok(out)
}
pub fn command_line_lossy(&self) -> String {
fn convert_word<S: AsRef<OsStr>>(word: S) -> String {
fn char_requires_quoting(c: char) -> bool {
if c.is_ascii_alphanumeric() {
return false;
}
let allowed_chars = "/-,:.=";
!allowed_chars.contains(c)
}
let s =
String::from_utf8_lossy(word.as_ref().as_bytes()).to_string();
if s.chars().any(char_requires_quoting) {
format!("'{}'", s)
} else {
s
}
}
let mut out = convert_word(&self.program);
for arg in &self.args {
out.push(' ');
out.push_str(&convert_word(arg));
}
out
}
}
impl Default for Command {
fn default() -> Self {
Command {
program: PathBuf::new(),
args: Vec::new(),
dir: None,
log_to: LogTo::Stdout,
log_command: true,
log_output_on_error: false,
check: true,
capture: false,
combine_output: false,
clear_env: false,
env: HashMap::new(),
}
}
}
impl From<&Command> for process::Command {
fn from(cmd: &Command) -> Self {
let mut out = process::Command::new(&cmd.program);
out.args(&cmd.args);
if let Some(dir) = &cmd.dir {
out.current_dir(dir);
}
if cmd.clear_env {
out.env_clear();
}
out.envs(&cmd.env);
out
}
}