#![deny(missing_debug_implementations)]
#![deny(missing_docs)]
#![deny(rust_2018_idioms)]
mod error;
use std::{
cell::RefCell,
collections::HashMap,
env::{self, current_dir, VarError},
ffi::{OsStr, OsString},
fmt, fs,
io::{self, ErrorKind, Write},
mem,
path::{Path, PathBuf},
process::{Command, ExitStatus, Output, Stdio},
sync::atomic::{AtomicUsize, Ordering},
};
pub use crate::error::{Error, Result};
#[doc(hidden)]
pub use xshell_macros::__cmd;
#[macro_export]
macro_rules! cmd {
($sh:expr, $cmd:literal) => {{
#[cfg(trick_rust_analyzer_into_highlighting_interpolated_bits)]
format_args!($cmd);
let f = |prog| $sh.cmd(prog);
let cmd: $crate::Cmd = $crate::__cmd!(f $cmd);
cmd
}};
}
#[derive(Debug)]
pub struct Shell {
cwd: RefCell<PathBuf>,
env: RefCell<HashMap<OsString, OsString>>,
}
impl std::panic::UnwindSafe for Shell {}
impl std::panic::RefUnwindSafe for Shell {}
impl Shell {
pub fn new() -> Result<Shell> {
let cwd = current_dir().map_err(|err| Error::new_current_dir(err, None))?;
let cwd = RefCell::new(cwd);
let env = RefCell::new(HashMap::new());
Ok(Shell { cwd, env })
}
#[doc(alias = "pwd")]
pub fn current_dir(&self) -> PathBuf {
self.cwd.borrow().clone()
}
#[doc(alias = "pwd")]
pub fn change_dir<P: AsRef<Path>>(&self, dir: P) {
self._change_dir(dir.as_ref())
}
fn _change_dir(&self, dir: &Path) {
let dir = self.path(dir);
*self.cwd.borrow_mut() = dir;
}
#[doc(alias = "pushd")]
pub fn push_dir<P: AsRef<Path>>(&self, path: P) -> PushDir<'_> {
self._push_dir(path.as_ref())
}
fn _push_dir(&self, path: &Path) -> PushDir<'_> {
let path = self.path(path);
PushDir::new(self, path)
}
pub fn var<K: AsRef<OsStr>>(&self, key: K) -> Result<String> {
self._var(key.as_ref())
}
fn _var(&self, key: &OsStr) -> Result<String> {
match self._var_os(key) {
Some(it) => it.into_string().map_err(VarError::NotUnicode),
None => Err(VarError::NotPresent),
}
.map_err(|err| Error::new_var(err, key.to_os_string()))
}
pub fn var_os<K: AsRef<OsStr>>(&self, key: K) -> Option<OsString> {
self._var_os(key.as_ref())
}
fn _var_os(&self, key: &OsStr) -> Option<OsString> {
self.env.borrow().get(key).cloned().or_else(|| env::var_os(key))
}
pub fn set_var<K: AsRef<OsStr>, V: AsRef<OsStr>>(&self, key: K, val: V) {
self._set_var(key.as_ref(), val.as_ref())
}
fn _set_var(&self, key: &OsStr, val: &OsStr) {
self.env.borrow_mut().insert(key.to_os_string(), val.to_os_string());
}
pub fn push_env<K: AsRef<OsStr>, V: AsRef<OsStr>>(&self, key: K, val: V) -> PushEnv<'_> {
self._push_env(key.as_ref(), val.as_ref())
}
fn _push_env(&self, key: &OsStr, val: &OsStr) -> PushEnv<'_> {
PushEnv::new(self, key.to_os_string(), val.to_os_string())
}
#[doc(alias = "cat")]
pub fn read_file<P: AsRef<Path>>(&self, path: P) -> Result<String> {
self._read_file(path.as_ref())
}
fn _read_file(&self, path: &Path) -> Result<String> {
let path = self.path(path);
fs::read_to_string(&path).map_err(|err| Error::new_read_file(err, path))
}
pub fn read_binary_file<P: AsRef<Path>>(&self, path: P) -> Result<Vec<u8>> {
self._read_binary_file(path.as_ref())
}
fn _read_binary_file(&self, path: &Path) -> Result<Vec<u8>> {
let path = self.path(path);
fs::read(&path).map_err(|err| Error::new_read_file(err, path))
}
#[doc(alias = "ls")]
pub fn read_dir<P: AsRef<Path>>(&self, path: P) -> Result<Vec<PathBuf>> {
self._read_dir(path.as_ref())
}
fn _read_dir(&self, path: &Path) -> Result<Vec<PathBuf>> {
let path = self.path(path);
let mut res = Vec::new();
|| -> _ {
for entry in fs::read_dir(&path)? {
let entry = entry?;
res.push(entry.path())
}
Ok(())
}()
.map_err(|err| Error::new_read_dir(err, path))?;
res.sort();
Ok(res)
}
pub fn write_file<P: AsRef<Path>, C: AsRef<[u8]>>(&self, path: P, contents: C) -> Result<()> {
self._write_file(path.as_ref(), contents.as_ref())
}
fn _write_file(&self, path: &Path, contents: &[u8]) -> Result<()> {
let path = self.path(path);
if let Some(p) = path.parent() {
self.create_dir(p)?;
}
fs::write(&path, contents).map_err(|err| Error::new_write_file(err, path))
}
#[doc(alias = "cp")]
pub fn copy_file<S: AsRef<Path>, D: AsRef<Path>>(&self, src: S, dst: D) -> Result<()> {
self._copy_file(src.as_ref(), dst.as_ref())
}
fn _copy_file(&self, src: &Path, dst: &Path) -> Result<()> {
let src = self.path(src);
let dst = self.path(dst);
let dst = dst.as_path();
let mut _tmp;
let mut dst = dst;
if dst.is_dir() {
if let Some(file_name) = src.file_name() {
_tmp = dst.join(file_name);
dst = &_tmp;
}
}
std::fs::copy(&src, dst)
.map_err(|err| Error::new_copy_file(err, src.to_path_buf(), dst.to_path_buf()))?;
Ok(())
}
#[doc(alias = "ln")]
pub fn hard_link<S: AsRef<Path>, D: AsRef<Path>>(&self, src: S, dst: D) -> Result<()> {
self._hard_link(src.as_ref(), dst.as_ref())
}
fn _hard_link(&self, src: &Path, dst: &Path) -> Result<()> {
let src = self.path(src);
let dst = self.path(dst);
fs::hard_link(&src, &dst).map_err(|err| Error::new_hard_link(err, src, dst))
}
#[doc(alias("mkdir_p", "mkdir"))]
pub fn create_dir<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf> {
self._create_dir(path.as_ref())
}
fn _create_dir(&self, path: &Path) -> Result<PathBuf> {
let path = self.path(path);
match fs::create_dir_all(&path) {
Ok(()) => Ok(path),
Err(err) => Err(Error::new_create_dir(err, path)),
}
}
#[doc(alias = "mktemp")]
pub fn create_temp_dir(&self) -> Result<TempDir> {
let base = std::env::temp_dir();
self.create_dir(&base)?;
static CNT: AtomicUsize = AtomicUsize::new(0);
let mut n_try = 0u32;
loop {
let cnt = CNT.fetch_add(1, Ordering::Relaxed);
let path = base.join(format!("xshell-tmp-dir-{}", cnt));
match fs::create_dir(&path) {
Ok(()) => return Ok(TempDir { path }),
Err(err) if n_try == 1024 => return Err(Error::new_create_dir(err, path)),
Err(_) => n_try += 1,
}
}
}
#[doc(alias("rm_rf", "rm"))]
pub fn remove_path<P: AsRef<Path>>(&self, path: P) -> Result<()> {
self._remove_path(path.as_ref())
}
fn _remove_path(&self, path: &Path) -> Result<(), Error> {
let path = self.path(path);
match path.metadata() {
Ok(meta) => if meta.is_dir() { remove_dir_all(&path) } else { fs::remove_file(&path) }
.map_err(|err| Error::new_remove_path(err, path)),
Err(err) if err.kind() == ErrorKind::NotFound => Ok(()),
Err(err) => Err(Error::new_remove_path(err, path)),
}
}
#[doc(alias("stat"))]
pub fn path_exists<P: AsRef<Path>>(&self, path: P) -> bool {
self.path(path.as_ref()).exists()
}
pub fn cmd<P: AsRef<Path>>(&self, program: P) -> Cmd<'_> {
Cmd::new(self, program.as_ref())
}
fn path(&self, p: &Path) -> PathBuf {
let cd = self.cwd.borrow();
cd.join(p)
}
}
#[derive(Debug)]
#[must_use]
pub struct PushDir<'a> {
old_cwd: PathBuf,
shell: &'a Shell,
}
impl<'a> PushDir<'a> {
fn new(shell: &'a Shell, path: PathBuf) -> PushDir<'a> {
PushDir { old_cwd: mem::replace(&mut *shell.cwd.borrow_mut(), path), shell }
}
}
impl Drop for PushDir<'_> {
fn drop(&mut self) {
mem::swap(&mut *self.shell.cwd.borrow_mut(), &mut self.old_cwd)
}
}
#[derive(Debug)]
#[must_use]
pub struct PushEnv<'a> {
key: OsString,
old_value: Option<OsString>,
shell: &'a Shell,
}
impl<'a> PushEnv<'a> {
fn new(shell: &'a Shell, key: OsString, val: OsString) -> PushEnv<'a> {
let old_value = shell.env.borrow_mut().insert(key.clone(), val);
PushEnv { shell, key, old_value }
}
}
impl Drop for PushEnv<'_> {
fn drop(&mut self) {
let mut env = self.shell.env.borrow_mut();
let key = mem::take(&mut self.key);
match self.old_value.take() {
Some(value) => {
env.insert(key, value);
}
None => {
env.remove(&key);
}
}
}
}
#[derive(Debug)]
#[must_use]
pub struct Cmd<'a> {
shell: &'a Shell,
data: CmdData,
}
#[derive(Debug, Default, Clone)]
struct CmdData {
prog: PathBuf,
args: Vec<OsString>,
env_changes: Vec<EnvChange>,
ignore_status: bool,
quiet: bool,
secret: bool,
stdin_contents: Option<Vec<u8>>,
ignore_stdout: bool,
ignore_stderr: bool,
}
#[derive(Debug, Clone)]
enum EnvChange {
Set(OsString, OsString),
Remove(OsString),
Clear,
}
impl fmt::Display for Cmd<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.data, f)
}
}
impl fmt::Display for CmdData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.secret {
return write!(f, "<secret>");
}
write!(f, "{}", self.prog.display())?;
for arg in &self.args {
let arg = arg.to_string_lossy();
if arg.chars().any(|it| it.is_ascii_whitespace()) {
write!(f, " \"{}\"", arg.escape_default())?
} else {
write!(f, " {}", arg)?
};
}
Ok(())
}
}
impl From<Cmd<'_>> for Command {
fn from(cmd: Cmd<'_>) -> Command {
cmd.to_command()
}
}
impl<'a> Cmd<'a> {
fn new(shell: &'a Shell, prog: &Path) -> Cmd<'a> {
let mut data = CmdData::default();
data.prog = prog.to_path_buf();
Cmd { shell, data }
}
pub fn arg<P: AsRef<OsStr>>(mut self, arg: P) -> Cmd<'a> {
self._arg(arg.as_ref());
self
}
fn _arg(&mut self, arg: &OsStr) {
self.data.args.push(arg.to_owned())
}
pub fn args<I>(mut self, args: I) -> Cmd<'a>
where
I: IntoIterator,
I::Item: AsRef<OsStr>,
{
args.into_iter().for_each(|it| self._arg(it.as_ref()));
self
}
#[doc(hidden)]
pub fn __extend_arg<P: AsRef<OsStr>>(mut self, arg_fragment: P) -> Cmd<'a> {
self.___extend_arg(arg_fragment.as_ref());
self
}
fn ___extend_arg(&mut self, arg_fragment: &OsStr) {
match self.data.args.last_mut() {
Some(last_arg) => last_arg.push(arg_fragment),
None => {
let mut prog = mem::take(&mut self.data.prog).into_os_string();
prog.push(arg_fragment);
self.data.prog = prog.into();
}
}
}
pub fn env<K: AsRef<OsStr>, V: AsRef<OsStr>>(mut self, key: K, val: V) -> Cmd<'a> {
self._env_set(key.as_ref(), val.as_ref());
self
}
fn _env_set(&mut self, key: &OsStr, val: &OsStr) {
self.data.env_changes.push(EnvChange::Set(key.to_owned(), val.to_owned()));
}
pub fn envs<I, K, V>(mut self, vars: I) -> Cmd<'a>
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: AsRef<OsStr>>(mut self, key: K) -> Cmd<'a> {
self._env_remove(key.as_ref());
self
}
fn _env_remove(&mut self, key: &OsStr) {
self.data.env_changes.push(EnvChange::Remove(key.to_owned()));
}
pub fn env_clear(mut self) -> Cmd<'a> {
self.data.env_changes.push(EnvChange::Clear);
self
}
pub fn ignore_status(mut self) -> Cmd<'a> {
self.set_ignore_status(true);
self
}
pub fn set_ignore_status(&mut self, yes: bool) {
self.data.ignore_status = yes;
}
pub fn quiet(mut self) -> Cmd<'a> {
self.set_quiet(true);
self
}
pub fn set_quiet(&mut self, yes: bool) {
self.data.quiet = yes;
}
pub fn secret(mut self) -> Cmd<'a> {
self.set_secret(true);
self
}
pub fn set_secret(&mut self, yes: bool) {
self.data.secret = yes;
}
pub fn stdin(mut self, stdin: impl AsRef<[u8]>) -> Cmd<'a> {
self._stdin(stdin.as_ref());
self
}
fn _stdin(&mut self, stdin: &[u8]) {
self.data.stdin_contents = Some(stdin.to_vec());
}
pub fn ignore_stdout(mut self) -> Cmd<'a> {
self.set_ignore_stdout(true);
self
}
pub fn set_ignore_stdout(&mut self, yes: bool) {
self.data.ignore_stdout = yes;
}
pub fn ignore_stderr(mut self) -> Cmd<'a> {
self.set_ignore_stderr(true);
self
}
pub fn set_ignore_stderr(&mut self, yes: bool) {
self.data.ignore_stderr = yes;
}
pub fn run(&self) -> Result<()> {
if !self.data.quiet {
eprintln!("$ {}", self);
}
self.output_impl(false, false).map(|_| ())
}
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> {
self.output_impl(true, true)
}
fn read_stream(&self, read_stderr: bool) -> Result<String> {
let read_stdout = !read_stderr;
let output = self.output_impl(read_stdout, read_stderr)?;
self.check_status(output.status)?;
let stream = if read_stderr { output.stderr } else { output.stdout };
let mut stream = String::from_utf8(stream).map_err(|err| Error::new_cmd_utf8(self, err))?;
if stream.ends_with('\n') {
stream.pop();
}
if stream.ends_with('\r') {
stream.pop();
}
Ok(stream)
}
fn output_impl(&self, read_stdout: bool, read_stderr: bool) -> Result<Output> {
let mut child = {
let mut command = self.to_command();
if !self.data.ignore_stdout {
command.stdout(if read_stdout { Stdio::piped() } else { Stdio::inherit() });
}
if !self.data.ignore_stderr {
command.stderr(if read_stderr { Stdio::piped() } else { Stdio::inherit() });
}
command.stdin(match &self.data.stdin_contents {
Some(_) => Stdio::piped(),
None => Stdio::null(),
});
command.spawn().map_err(|err| {
if matches!(err.kind(), io::ErrorKind::NotFound) {
let cwd = self.shell.cwd.borrow();
if let Err(err) = cwd.metadata() {
return Error::new_current_dir(err, Some(cwd.clone()));
}
}
Error::new_cmd_io(self, err)
})?
};
let mut io_thread = None;
if let Some(stdin_contents) = self.data.stdin_contents.clone() {
let mut stdin = child.stdin.take().unwrap();
io_thread = Some(std::thread::spawn(move || {
stdin.write_all(&stdin_contents)?;
stdin.flush()
}));
}
let out_res = child.wait_with_output();
let err_res = io_thread.map(|it| it.join().unwrap());
let output = out_res.map_err(|err| Error::new_cmd_io(self, err))?;
if let Some(err_res) = err_res {
err_res.map_err(|err| Error::new_cmd_stdin(self, err))?;
}
self.check_status(output.status)?;
Ok(output)
}
fn to_command(&self) -> Command {
let mut res = Command::new(&self.data.prog);
res.current_dir(self.shell.current_dir());
res.args(&self.data.args);
for (key, val) in &*self.shell.env.borrow() {
res.env(key, val);
}
for change in &self.data.env_changes {
match change {
EnvChange::Clear => res.env_clear(),
EnvChange::Remove(key) => res.env_remove(key),
EnvChange::Set(key, val) => res.env(key, val),
};
}
if self.data.ignore_stdout {
res.stdout(Stdio::null());
}
if self.data.ignore_stderr {
res.stderr(Stdio::null());
}
res
}
fn check_status(&self, status: ExitStatus) -> Result<()> {
if status.success() || self.data.ignore_status {
return Ok(());
}
Err(Error::new_cmd_status(self, status))
}
}
#[derive(Debug)]
#[must_use]
pub struct TempDir {
path: PathBuf,
}
impl TempDir {
pub fn path(&self) -> &Path {
&self.path
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = remove_dir_all(&self.path);
}
}
#[cfg(not(windows))]
fn remove_dir_all(path: &Path) -> io::Result<()> {
std::fs::remove_dir_all(path)
}
#[cfg(windows)]
fn remove_dir_all(path: &Path) -> io::Result<()> {
for _ in 0..99 {
if fs::remove_dir_all(path).is_ok() {
return Ok(());
}
std::thread::sleep(std::time::Duration::from_millis(10))
}
fs::remove_dir_all(path)
}