#![deny(missing_debug_implementations)]
#![deny(missing_docs)]
#![deny(rust_2018_idioms)]
mod env;
mod gsl;
mod error;
mod fs;
use std::{
ffi::{OsStr, OsString},
fmt, io,
io::Write,
path::Path,
process::Output,
process::Stdio,
};
use error::CmdErrorKind;
#[doc(hidden)]
pub use xshell_macros::__cmd;
pub use crate::{
env::{pushd, pushenv, Pushd, Pushenv},
error::{Error, Result},
fs::{cp, cwd, mkdir_p, mktemp_d, read_dir, read_file, rm_rf, write_file, TempDir},
};
#[macro_export]
macro_rules! cmd {
($cmd:tt) => {{
#[cfg(trick_rust_analyzer_into_highlighting_interpolated_bits)]
format_args!($cmd);
use $crate::Cmd as __CMD;
let cmd: $crate::Cmd = $crate::__cmd!(__CMD $cmd);
cmd
}};
}
#[must_use]
#[derive(Debug)]
pub struct Cmd {
args: Vec<OsString>,
stdin_contents: Option<Vec<u8>>,
ignore_status: bool,
echo_cmd: bool,
secret: bool,
env_changes: Vec<EnvChange>,
ignore_stdout: bool,
ignore_stderr: bool,
}
impl fmt::Display for Cmd {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if !self.secret {
let mut space = "";
for arg in &self.args {
write!(f, "{}", space)?;
space = " ";
let arg = arg.to_string_lossy();
if arg.chars().any(|it| it.is_ascii_whitespace()) {
write!(f, "\"{}\"", arg.escape_default())?
} else {
write!(f, "{}", arg)?
};
}
} else {
write!(f, "<secret>")?;
}
Ok(())
}
}
impl From<Cmd> for std::process::Command {
fn from(cmd: Cmd) -> Self {
cmd.command()
}
}
impl Cmd {
pub fn new(program: impl AsRef<Path>) -> Cmd {
Cmd::_new(program.as_ref())
}
fn _new(program: &Path) -> Cmd {
Cmd {
args: vec![program.as_os_str().to_owned()],
stdin_contents: None,
ignore_status: false,
echo_cmd: true,
secret: false,
env_changes: vec![],
ignore_stdout: false,
ignore_stderr: false,
}
}
pub fn arg(mut self, arg: impl AsRef<OsStr>) -> Cmd {
self._arg(arg.as_ref());
self
}
pub fn args<I>(mut self, args: I) -> Cmd
where
I: IntoIterator,
I::Item: AsRef<OsStr>,
{
args.into_iter().for_each(|it| self._arg(it.as_ref()));
self
}
fn _arg(&mut self, arg: &OsStr) {
self.args.push(arg.to_owned())
}
pub fn env<K, V>(mut self, key: K, val: V) -> Cmd
where
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
self._env_set(key.as_ref(), val.as_ref());
self
}
fn _env_set(&mut self, key: &OsStr, val: &OsStr) {
self.env_changes.push(EnvChange::Set(key.to_owned(), val.to_owned()));
}
pub fn envs<I, K, V>(mut self, vars: I) -> Cmd
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
vars.into_iter().for_each(|(k, v)| self._env_set(k.as_ref(), v.as_ref()));
self
}
pub fn env_remove<K>(mut self, key: K) -> Cmd
where
K: AsRef<OsStr>,
{
self._env_remove(key.as_ref());
self
}
fn _env_remove(&mut self, key: &OsStr) {
self.env_changes.push(EnvChange::Remove(key.to_owned()));
}
pub fn env_clear(mut self) -> Cmd {
self.env_changes.push(EnvChange::Clear);
self
}
pub fn ignore_stdout(mut self) -> Cmd {
self.ignore_stdout = true;
self
}
pub fn ignore_stderr(mut self) -> Cmd {
self.ignore_stderr = true;
self
}
#[doc(hidden)]
pub fn __extend_arg(mut self, arg: impl AsRef<OsStr>) -> Cmd {
self.___extend_arg(arg.as_ref());
self
}
fn ___extend_arg(&mut self, arg: &OsStr) {
self.args.last_mut().unwrap().push(arg)
}
pub fn ignore_status(mut self) -> Cmd {
self.ignore_status = true;
self
}
pub fn stdin(mut self, stdin: impl AsRef<[u8]>) -> Cmd {
self._stdin(stdin.as_ref());
self
}
fn _stdin(&mut self, stdin: &[u8]) {
self.stdin_contents = Some(stdin.to_vec());
}
pub fn echo_cmd(mut self, echo: bool) -> Cmd {
self.echo_cmd = echo;
self
}
pub fn secret(mut self, secret: bool) -> Cmd {
self.secret = secret;
self
}
pub fn read(self) -> Result<String> {
self.read_stream(false)
}
pub fn read_stderr(self) -> Result<String> {
self.read_stream(true)
}
pub fn output(self) -> Result<Output> {
match self.output_impl(true, true) {
Ok(output) if output.status.success() || self.ignore_status => Ok(output),
Ok(output) => Err(CmdErrorKind::NonZeroStatus(output.status).err(self)),
Err(io_err) => Err(CmdErrorKind::Io(io_err).err(self)),
}
}
pub fn run(self) -> Result<()> {
let _guard = gsl::read();
if self.echo_cmd {
eprintln!("$ {}", self);
}
match self.command().status() {
Ok(status) if status.success() || self.ignore_status => Ok(()),
Ok(status) => Err(CmdErrorKind::NonZeroStatus(status).err(self)),
Err(io_err) => Err(CmdErrorKind::Io(io_err).err(self)),
}
}
fn read_stream(self, read_stderr: bool) -> Result<String> {
let read_stdout = !read_stderr;
match self.output_impl(read_stdout, read_stderr) {
Ok(output) if output.status.success() || self.ignore_status => {
let stream = if read_stderr { output.stderr } else { output.stdout };
let mut stream = String::from_utf8(stream)
.map_err(|utf8_err| CmdErrorKind::NonUtf8Output(utf8_err).err(self))?;
if stream.ends_with('\n') {
stream.pop();
}
if stream.ends_with('\r') {
stream.pop();
}
Ok(stream)
}
Ok(output) => Err(CmdErrorKind::NonZeroStatus(output.status).err(self)),
Err(io_err) => Err(CmdErrorKind::Io(io_err).err(self)),
}
}
fn output_impl(&self, read_stdout: bool, read_stderr: bool) -> io::Result<Output> {
let mut child = {
let _guard = gsl::read();
let mut command = self.command();
command.stdin(match &self.stdin_contents {
Some(_) => Stdio::piped(),
None => Stdio::null(),
});
if !self.ignore_stdout {
command.stdout(if read_stdout { Stdio::piped() } else { Stdio::inherit() });
}
if !self.ignore_stderr {
command.stderr(if read_stderr { Stdio::piped() } else { Stdio::inherit() });
}
command.spawn()?
};
if let Some(stdin_contents) = &self.stdin_contents {
let mut stdin = child.stdin.take().unwrap();
stdin.write_all(stdin_contents)?;
stdin.flush()?;
}
child.wait_with_output()
}
fn command(&self) -> std::process::Command {
let mut res = std::process::Command::new(&self.args[0]);
res.args(&self.args[1..]);
self.apply_env(&mut res);
if self.ignore_stdout {
res.stdout(Stdio::null());
}
if self.ignore_stderr {
res.stderr(Stdio::null());
}
res
}
fn apply_env(&self, cmd: &mut std::process::Command) {
for change in &self.env_changes {
match change {
EnvChange::Clear => cmd.env_clear(),
EnvChange::Remove(key) => cmd.env_remove(key),
EnvChange::Set(key, val) => cmd.env(key, val),
};
}
}
}
#[derive(Debug)]
enum EnvChange {
Set(OsString, OsString),
Remove(OsString),
Clear,
}