#![warn(missing_docs)]
#![warn(rust_2018_idioms)]
#![forbid(unsafe_code)]
use std::{
borrow::Cow,
collections::HashMap,
env,
ffi::OsString,
fmt,
io::{self, Write},
iter,
path::{Path, PathBuf},
process::Stdio,
sync::Arc,
};
pub use error::*;
use io::BufRead;
#[macro_export]
macro_rules! cmd {
($bin:expr $(, $arg:expr )* $(,)?) => {{
let mut cmd = $crate::Cmd::new($bin);
$(cmd.arg($arg);)*
cmd
}};
}
#[macro_export]
macro_rules! run {
($($params:tt)*) => {{ $crate::cmd!($($params)*).run() }}
}
#[macro_export]
macro_rules! read {
($($params:tt)*) => {{ $crate::cmd!($($params)*).read() }}
}
#[macro_export]
macro_rules! read_bytes {
($($params:tt)*) => {{ $crate::cmd!($($params)*).read_bytes() }}
}
mod error;
#[derive(Clone)]
enum BinOrUtf8 {
Bin(Vec<u8>),
Utf8(String),
}
impl fmt::Display for BinOrUtf8 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BinOrUtf8::Bin(bytes) => write!(f, "[bytes]:\n{:?}", bytes),
BinOrUtf8::Utf8(utf8) => write!(f, "[utf8]:\n{}", utf8),
}
}
}
impl AsRef<[u8]> for BinOrUtf8 {
fn as_ref(&self) -> &[u8] {
match self {
BinOrUtf8::Bin(it) => it.as_ref(),
BinOrUtf8::Utf8(it) => it.as_ref(),
}
}
}
#[must_use = "commands are not executed until run(), read() or spawn() is called"]
#[derive(Clone)]
pub struct Cmd(Arc<CmdShared>);
#[derive(Clone)]
struct CmdShared {
bin: PathBuf,
args: Vec<OsString>,
env: HashMap<OsString, OsString>,
stdin: Option<BinOrUtf8>,
current_dir: Option<PathBuf>,
log_cmd: Option<log::Level>,
log_err: Option<log::Level>,
}
impl fmt::Debug for Cmd {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
impl fmt::Display for Cmd {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", (self.0).bin.display())?;
for arg in &(self.0).args {
let arg = arg.to_string_lossy();
if arg.chars().any(char::is_whitespace) {
write!(f, " '{}'", arg)?;
} else {
write!(f, " {}", arg)?;
}
}
if let Some(dir) = &self.0.current_dir {
write!(f, "\n(at {})", dir.display())?;
}
if !self.0.env.is_empty() {
write!(f, "\nenv: {:#?}", self.0.env)?;
}
if let Some(stdin) = &self.0.stdin {
write!(f, "\nstdin <<< {}", stdin)?;
}
Ok(())
}
}
impl Cmd {
pub fn new(bin: impl Into<PathBuf>) -> Self {
Self(Arc::new(CmdShared {
bin: bin.into(),
args: Vec::new(),
env: HashMap::default(),
log_cmd: Some(log::Level::Debug),
log_err: Some(log::Level::Error),
stdin: None,
current_dir: None,
}))
}
pub fn try_at(bin_path: impl Into<PathBuf>) -> Option<Self> {
Self::_try_at(bin_path.into())
}
fn _try_at(bin: PathBuf) -> Option<Self> {
let with_extension = match env::consts::EXE_EXTENSION {
"" => None,
it if bin.extension().is_none() => Some(bin.with_extension(it)),
_ => None,
};
iter::once(bin)
.chain(with_extension)
.find(|it| it.is_file())
.map(Self::new)
}
pub fn lookup_in_path(bin_name: &str) -> Option<Self> {
let paths = env::var_os("PATH").unwrap_or_default();
env::split_paths(&paths)
.map(|path| path.join(bin_name))
.find_map(Self::try_at)
}
fn as_mut(&mut self) -> &mut CmdShared {
Arc::make_mut(&mut self.0)
}
pub fn bin(&mut self, bin: impl Into<PathBuf>) -> &mut Self {
self.as_mut().bin = bin.into();
self
}
pub fn get_bin(&self) -> &Path {
&self.0.bin
}
pub fn current_dir(&mut self, dir: impl Into<PathBuf>) -> &mut Self {
self.as_mut().current_dir = Some(dir.into());
self
}
pub fn get_current_dir(&self) -> Option<&Path> {
self.0.current_dir.as_deref()
}
pub fn log_cmd(&mut self, level: impl Into<Option<log::Level>>) -> &mut Self {
self.as_mut().log_cmd = level.into();
self
}
pub fn log_err(&mut self, level: impl Into<Option<log::Level>>) -> &mut Self {
self.as_mut().log_err = level.into();
self
}
pub fn stdin(&mut self, stdin: impl Into<String>) -> &mut Self {
self.as_mut().stdin = Some(BinOrUtf8::Utf8(stdin.into()));
self
}
pub fn stdin_bytes(&mut self, stdin: Vec<u8>) -> &mut Self {
self.as_mut().stdin = Some(BinOrUtf8::Bin(stdin));
self
}
pub fn arg2(&mut self, arg1: impl Into<OsString>, arg2: impl Into<OsString>) -> &mut Self {
self.arg(arg1).arg(arg2)
}
pub fn arg(&mut self, arg: impl Into<OsString>) -> &mut Self {
self.as_mut().args.push(arg.into());
self
}
pub fn replace_arg(&mut self, idx: usize, arg: impl Into<OsString>) -> &mut Self {
self.as_mut().args[idx] = arg.into();
self
}
pub fn args<I>(&mut self, args: I) -> &mut Self
where
I: IntoIterator,
I::Item: Into<OsString>,
{
self.as_mut().args.extend(args.into_iter().map(Into::into));
self
}
pub fn get_args(&self) -> &[OsString] {
&self.0.args
}
pub fn env(&mut self, key: impl Into<OsString>, val: impl Into<OsString>) -> &mut Self {
self.as_mut().env.insert(key.into(), val.into());
self
}
pub fn run(&self) -> Result<()> {
self.spawn()?.wait()?;
Ok(())
}
pub fn read(&self) -> Result<String> {
self.spawn_piped()?.read()
}
pub fn read_bytes(&self) -> Result<Vec<u8>> {
self.spawn_piped()?.read_bytes()
}
pub fn spawn(&self) -> Result<Child> {
self.spawn_with(Stdio::inherit(), Stdio::inherit())
}
pub fn spawn_piped(&self) -> Result<Child> {
self.spawn_with(Stdio::piped(), Stdio::inherit())
}
pub fn spawn_with(&self, stdout: Stdio, stderr: Stdio) -> Result<Child> {
let mut cmd = std::process::Command::new(&self.0.bin);
cmd.args(&self.0.args)
.envs(&self.0.env)
.stderr(stderr)
.stdout(stdout);
if let Some(dir) = &self.0.current_dir {
cmd.current_dir(dir);
}
let child = match &self.0.stdin {
None => cmd.stdin(Stdio::null()).spawn().cmd_context(self)?,
Some(_) => {
cmd.stdin(Stdio::piped());
cmd.spawn().cmd_context(self)?
}
};
let mut child = Child {
cmd: Cmd(Arc::clone(&self.0)),
child,
};
if let Some(level) = self.0.log_cmd {
log::log!(level, "{}", child);
}
if let Some(stdin) = &self.0.stdin {
child
.child
.stdin
.take()
.unwrap()
.write_all(stdin.as_ref())
.cmd_context(self)?;
}
Ok(child)
}
fn bin_name(&self) -> Cow<'_, str> {
self.0
.bin
.components()
.last()
.expect("Binary name must not be empty")
.as_os_str()
.to_string_lossy()
}
}
pub struct Child {
cmd: Cmd,
child: std::process::Child,
}
impl Drop for Child {
fn drop(&mut self) {
match self.child.try_wait() {
Ok(None) => {
log::debug!("[KILL {}] {}", self.child.id(), self.cmd.bin_name());
let _ = self.child.kill();
self.child.wait().unwrap_or_else(|err| {
panic!("Failed to wait for process: {}\nProcess: {}", err, self);
});
}
Ok(Some(_status)) => {}
Err(err) => panic!("Failed to collect process exit status: {}", err),
}
}
}
impl fmt::Display for Child {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let id = self.child.id();
write!(f, "[PID {}] {}", id, self.cmd)
}
}
impl Child {
pub fn cmd(&self) -> Cmd {
self.cmd.clone()
}
pub fn wait(&mut self) -> Result<()> {
let exit_status = self.child.wait().proc_context(self)?;
if !exit_status.success() {
return Err(Error::proc(
&self,
&format_args!("Non-zero exit code: {}", exit_status),
));
}
Ok(())
}
pub fn read_bytes(mut self) -> Result<Vec<u8>> {
let output = self.read_bytes_no_wait(Ostream::StdOut)?;
self.wait()?;
Ok(output)
}
pub fn read(mut self) -> Result<String> {
let output = self.read_no_wait(Ostream::StdOut)?;
self.wait()?;
Ok(output)
}
fn expect_ostream(&mut self, ostream: Ostream) -> &mut dyn io::Read {
match ostream {
Ostream::StdOut => {
if let Some(ref mut it) = self.child.stdout {
return it;
}
}
Ostream::StdErr => {
if let Some(ref mut it) = self.child.stderr {
return it;
}
}
};
panic!("{} wasn't piped for {}", ostream, self);
}
pub fn read_no_wait(&mut self, ostream: Ostream) -> Result<String> {
let mut output = String::new();
self.expect_ostream(ostream)
.read_to_string(&mut output)
.map_err(|err| self.io_read_err(err, ostream, "utf8"))?;
self.log_output(ostream, &format_args!("[utf8]:\n{}", output));
Ok(output)
}
pub fn read_bytes_no_wait(&mut self, ostream: Ostream) -> Result<Vec<u8>> {
let mut output = Vec::new();
self.expect_ostream(ostream)
.read_to_end(&mut output)
.map_err(|err| self.io_read_err(err, ostream, "bytes"))?;
self.log_output(ostream, &format_args!("[bytes]:\n{:?}", output));
Ok(output)
}
fn io_read_err(&self, err: io::Error, ostream: Ostream, data_kind: &str) -> Error {
Error::proc(
self,
&format_args!("Failed to read {} from `{}`: {}", data_kind, ostream, err),
)
}
fn log_output(&self, ostream: Ostream, output: &dyn fmt::Display) {
if let Some(level) = self.cmd.0.log_cmd {
let pid = self.child.id();
let bin_name = self.cmd.bin_name();
log::log!(level, "[{} {} {}] {}", ostream, pid, bin_name, output,);
}
}
pub fn stdout_lines(&mut self) -> impl Iterator<Item = String> + '_ {
let log_cmd = self.cmd.0.log_cmd;
let id = self.child.id();
let bin_name = self.cmd.bin_name();
let stdout = io::BufReader::new(self.child.stdout.as_mut().unwrap());
stdout
.lines()
.map(|line| line.expect("Unexpected io error"))
.inspect(move |line| {
if let Some(level) = log_cmd {
log::log!(level, "[{} {}] {}", id, bin_name, line);
}
})
}
}
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub enum Ostream {
StdOut,
StdErr,
}
impl fmt::Display for Ostream {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Ostream::StdOut => "stdout",
Ostream::StdErr => "stderr",
})
}
}