#![warn(missing_docs)]
#![warn(rust_2018_idioms)]
#![forbid(unsafe_code)]
use std::{
borrow::Cow,
env,
ffi::OsString,
fmt,
io::{self, Read, Write},
iter,
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>,
stdin: Option<BinOrUtf8>,
current_dir: Option<PathBuf>,
echo_cmd: bool,
echo_err: bool,
}
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 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(),
echo_cmd: true,
echo_err: true,
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 current_dir(&mut self, dir: impl Into<PathBuf>) -> &mut Self {
self.as_mut().current_dir = Some(dir.into());
self
}
pub fn echo_cmd(&mut self, yes: bool) -> &mut Self {
self.as_mut().echo_cmd = yes;
self
}
pub fn echo_err(&mut self, yes: bool) -> &mut Self {
self.as_mut().echo_err = yes;
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 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())
}
pub fn spawn_piped(&self) -> Result<Child> {
self.spawn_with(Stdio::piped())
}
fn spawn_with(&self, stdout: Stdio) -> Result<Child> {
let mut cmd = std::process::Command::new(&self.0.bin);
cmd.args(&self.0.args)
.stderr(Stdio::inherit())
.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 self.0.echo_cmd {
eprintln!("{}", 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) => {
eprintln!("[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 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(self) -> Result<Vec<u8>> {
match self.read_impl(false)? {
BinOrUtf8::Utf8(_) => unreachable!(),
BinOrUtf8::Bin(it) => Ok(it),
}
}
pub fn read(self) -> Result<String> {
match self.read_impl(true)? {
BinOrUtf8::Utf8(it) => Ok(it),
BinOrUtf8::Bin(_) => unreachable!(),
}
}
fn read_impl(mut self, expect_utf8: bool) -> Result<BinOrUtf8> {
let stdout = {
let stdout = self
.child
.stdout
.as_mut()
.expect("use spawn_piped() to capture stdout instead of spawn()");
if expect_utf8 {
let mut out = String::new();
stdout.read_to_string(&mut out).proc_context(&self)?;
BinOrUtf8::Utf8(out)
} else {
let mut out = Vec::new();
stdout.read_to_end(&mut out).proc_context(&self)?;
BinOrUtf8::Bin(out)
}
};
self.wait()?;
if self.cmd.0.echo_cmd {
eprintln!("[STDOUT {}] {}", self.cmd.bin_name(), &stdout);
}
Ok(stdout)
}
pub fn stdout_lines(&mut self) -> impl Iterator<Item = String> + '_ {
let echo = self.cmd.0.echo_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 echo {
eprintln!("[{} {}] {}", id, bin_name, line);
}
})
}
}