#![deny(missing_docs)]
use std::borrow::Cow;
use std::collections::HashMap;
use std::ffi::{OsStr, OsString};
use std::os::unix::ffi::OsStrExt;
use std::path::PathBuf;
use std::{fmt, io, process};
#[derive(Debug)]
pub enum ErrorKind {
Launch(io::Error),
Exit(process::ExitStatus),
}
#[derive(Debug)]
pub struct Error {
pub command: Command,
pub kind: ErrorKind,
}
impl Error {
pub fn is_launch_error(&self) -> bool {
matches!(self.kind, ErrorKind::Launch(_))
}
pub fn is_exit_error(&self) -> bool {
matches!(self.kind, ErrorKind::Exit(_))
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match &self.kind {
ErrorKind::Launch(err) => write!(
f,
"failed to launch '{}': {}",
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,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Command {
pub program: PathBuf,
pub args: Vec<OsString>,
pub dir: Option<PathBuf>,
pub log_command: bool,
pub print_command: bool,
pub check: bool,
pub capture: 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 run(&self) -> Result<Output, Error> {
let cmd_str = self.command_line_lossy();
#[cfg(feature = "logging")]
if self.log_command {
log::info!("{}", cmd_str);
}
if self.print_command {
println!("{}", cmd_str);
}
let mut cmd: process::Command = self.into();
let out = if self.capture {
cmd.output()
.map_err(|err| Error {
command: self.clone(),
kind: ErrorKind::Launch(err),
})?
.into()
} else {
let status = cmd.status().map_err(|err| Error {
command: self.clone(),
kind: ErrorKind::Launch(err),
})?;
Output {
stdout: Vec::new(),
stderr: Vec::new(),
status,
}
};
if self.check && !out.status.success() {
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_command: false,
print_command: true,
check: true,
capture: true,
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
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn test_check() {
let mut cmd = Command::new("true");
assert!(cmd.run().is_ok());
cmd.program = Path::new("false").into();
assert!(cmd.run().unwrap_err().is_exit_error());
cmd.check = false;
assert!(cmd.run().is_ok());
}
#[test]
fn test_args() {
let out = Command::with_args("echo", &["hello", "world"])
.run()
.unwrap();
assert_eq!(out.stdout, b"hello world\n");
}
#[test]
fn test_add_arg_variations() {
let mut cmd = Command::new("a");
cmd.add_arg("b");
cmd.add_arg_pair("c", Path::new("d"));
cmd.add_args(&["e", "f", "g"]);
assert_eq!(cmd.command_line_lossy(), "a b c d e f g");
}
#[test]
fn test_command_line() {
assert_eq!(Command::new("test").command_line_lossy(), "test");
assert_eq!(
Command::with_args("test", &["hello", "world"])
.command_line_lossy(),
"test hello world"
);
assert_eq!(
Command::with_args("a b", &["c d", "e"]).command_line_lossy(),
"'a b' 'c d' e"
);
assert_eq!(
Command::with_args("a", &["-/,:"]).command_line_lossy(),
"a -/,:"
);
}
}