#![deny(missing_debug_implementations)]
#![deny(missing_docs)]
#![deny(rust_2018_idioms)]
mod exec;
mod error;
use std::{
collections::HashMap,
env::{self, current_dir, VarError},
ffi::{OsStr, OsString},
fmt::{self},
fs,
io::{self, ErrorKind},
mem,
path::{Path, PathBuf},
process::{Command, Output, Stdio},
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
time::{Duration, Instant},
};
pub use crate::error::{Error, Result};
use error::CmdErrorKind;
#[doc(hidden)]
pub use xshell_macros::__cmd;
const STREAM_SUFFIX_SIZE: usize = 128 * 1024;
#[macro_export]
macro_rules! cmd {
($sh:expr, $cmd:literal) => {{
#[cfg(any())] format_args!($cmd);
let f = |prog| $sh.cmd(prog);
let cmd: $crate::Cmd = $crate::__cmd!(f $cmd);
cmd
}};
}
#[derive(Debug, Clone)]
pub struct Shell {
cwd: Arc<Path>,
env: Arc<HashMap<Arc<OsStr>, Arc<OsStr>>>,
}
impl Shell {
pub fn new() -> Result<Shell> {
let cwd = current_dir().map_err(|err| Error::new_current_dir(err, None))?;
Ok(Shell { cwd: cwd.into(), env: Default::default() })
}
#[doc(alias = "pwd")]
pub fn current_dir(&self) -> &Path {
self.cwd.as_ref()
}
#[doc(alias = "cd")]
pub fn set_current_dir(&mut self, path: impl AsRef<Path>) {
fn inner(sh: &mut Shell, path: &OsStr) {
sh.cwd = sh.cwd.join(path).into();
}
inner(self, path.as_ref().as_os_str());
}
#[doc(alias = "pushd")]
#[must_use]
pub fn with_current_dir(&self, path: impl AsRef<Path>) -> Shell {
fn inner(sh: &Shell, path: &OsStr) -> Shell {
Shell { cwd: sh.cwd.join(path).into(), env: sh.env.clone() }
}
inner(self, path.as_ref().as_os_str())
}
pub fn var(&self, key: impl AsRef<OsStr>) -> Result<String> {
fn inner(sh: &Shell, key: &OsStr) -> Result<String> {
let env_os = sh
.var_os(key)
.ok_or(VarError::NotPresent)
.map_err(|err| Error::new_var(err, key.to_os_string()))?;
env_os
.into_string()
.map_err(|value| Error::new_var(VarError::NotUnicode(value), key.to_os_string()))
}
inner(self, key.as_ref())
}
pub fn var_os(&self, key: impl AsRef<OsStr>) -> Option<OsString> {
fn inner(sh: &Shell, key: &OsStr) -> Option<OsString> {
sh.env.get(key).map(OsString::from).or_else(|| env::var_os(key))
}
inner(self, key.as_ref())
}
pub fn vars_os(&self) -> HashMap<OsString, OsString> {
let mut result: HashMap<OsString, OsString> = Default::default();
result.extend(env::vars_os());
result.extend(self.env.iter().map(|(k, v)| (OsString::from(k), OsString::from(v))));
result
}
pub fn set_var(&mut self, key: impl AsRef<OsStr>, value: impl AsRef<OsStr>) {
fn inner(sh: &mut Shell, key: &OsStr, value: &OsStr) {
Arc::make_mut(&mut sh.env).insert(key.into(), value.into());
}
inner(self, key.as_ref(), value.as_ref());
}
pub fn with_var(&self, key: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> Shell {
fn inner(sh: &Shell, key: &OsStr, value: &OsStr) -> Shell {
let mut env = Arc::clone(&sh.env);
Arc::make_mut(&mut env).insert(key.into(), value.into());
Shell { cwd: sh.cwd.clone(), env }
}
inner(self, key.as_ref(), value.as_ref())
}
#[doc(alias = "cat")]
pub fn read_file(&self, path: impl AsRef<Path>) -> Result<String> {
fn inner(sh: &Shell, path: &Path) -> Result<String> {
let path = sh.path(path);
fs::read_to_string(&path).map_err(|err| Error::new_read_file(err, path))
}
inner(self, path.as_ref())
}
pub fn read_binary_file(&self, path: impl AsRef<Path>) -> Result<Vec<u8>> {
fn inner(sh: &Shell, path: &Path) -> Result<Vec<u8>> {
let path = sh.path(path);
fs::read(&path).map_err(|err| Error::new_read_file(err, path))
}
inner(self, path.as_ref())
}
pub fn write_file(&self, path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Result<()> {
fn inner(sh: &Shell, path: &Path, contents: &[u8]) -> Result<()> {
let path = sh.path(path);
if let Some(p) = path.parent() {
sh.create_dir(p)?;
}
fs::write(&path, contents).map_err(|err| Error::new_write_file(err, path))
}
inner(self, path.as_ref(), contents.as_ref())
}
#[doc(alias = "cp")]
pub fn copy_file(&self, src_file: impl AsRef<Path>, dst_file: impl AsRef<Path>) -> Result<()> {
fn inner(sh: &Shell, src: &Path, dst: &Path) -> Result<()> {
let src = sh.path(src);
let dst = sh.path(dst);
if let Some(p) = dst.parent() {
sh.create_dir(p)?;
}
std::fs::copy(&src, &dst)
.map_err(|err| Error::new_copy_file(err, src.to_path_buf(), dst.to_path_buf()))?;
Ok(())
}
inner(self, src_file.as_ref(), dst_file.as_ref())
}
#[doc(alias = "cp")]
pub fn copy_file_to_dir(
&self,
src_file: impl AsRef<Path>,
dst_dir: impl AsRef<Path>,
) -> Result<()> {
fn inner(sh: &Shell, src: &Path, dst: &Path) -> Result<()> {
let src = sh.path(src);
let dst = sh.path(dst);
let Some(file_name) = src.file_name() else {
return Err(Error::new_copy_file(io::ErrorKind::InvalidData.into(), src, dst));
};
sh.copy_file(&src, &dst.join(file_name))
}
inner(self, src_file.as_ref(), dst_dir.as_ref())
}
#[doc(alias = "ln")]
pub fn hard_link(&self, src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<()> {
fn inner(sh: &Shell, src: &Path, dst: &Path) -> Result<()> {
let src = sh.path(src);
let dst = sh.path(dst);
fs::hard_link(&src, &dst).map_err(|err| Error::new_hard_link(err, src, dst))
}
inner(self, src.as_ref(), dst.as_ref())
}
#[doc(alias = "ls")]
pub fn read_dir(&self, path: impl AsRef<Path>) -> Result<Vec<PathBuf>> {
fn inner(sh: &Shell, path: &Path) -> Result<Vec<PathBuf>> {
let path = sh.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)
}
inner(self, path.as_ref())
}
#[doc(alias("mkdir_p", "mkdir"))]
pub fn create_dir<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf> {
fn inner(sh: &Shell, path: &Path) -> Result<PathBuf> {
let path = sh.path(path);
match fs::create_dir_all(&path) {
Ok(()) => Ok(path),
Err(err) => Err(Error::new_create_dir(err, path)),
}
}
inner(self, path.as_ref())
}
#[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 try_count = 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 try_count == 1024 => return Err(Error::new_create_dir(err, path)),
Err(_) => try_count += 1,
}
}
}
#[doc(alias("rm_rf", "rm"))]
pub fn remove_path(&self, path: impl AsRef<Path>) -> Result<()> {
fn inner(sh: &Shell, path: &Path) -> Result<(), Error> {
let path = sh.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)),
}
}
inner(self, path.as_ref())
}
#[doc(alias("stat"))]
pub fn path_exists<P: AsRef<Path>>(&self, path: P) -> bool {
self.path(path.as_ref()).exists()
}
pub fn cmd(&self, program: impl AsRef<OsStr>) -> Cmd {
Cmd::new(self, program.as_ref())
}
fn path(&self, p: &Path) -> PathBuf {
self.cwd.join(p)
}
}
#[derive(Debug, Clone)]
#[must_use]
pub struct Cmd {
sh: Shell,
prog: PathBuf,
args: Vec<OsString>,
stdin_contents: Option<Vec<u8>>,
deadline: Option<Instant>,
ignore_status: bool,
secret: bool,
}
impl fmt::Display for Cmd {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.secret {
return write!(f, "<secret>");
}
write!(f, "{}", self.prog.as_path().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 Cmd {
fn new(sh: &Shell, program: impl AsRef<Path>) -> Cmd {
fn inner(sh: &Shell, program: &Path) -> Cmd {
Cmd {
sh: sh.clone(),
prog: program.into(),
args: Vec::new(),
stdin_contents: None,
ignore_status: false,
deadline: None,
secret: false,
}
}
inner(sh, program.as_ref())
}
pub fn arg(mut self, arg: impl AsRef<OsStr>) -> Cmd {
self.arg_inner(arg.as_ref());
self
}
fn arg_inner(&mut self, arg: &OsStr) {
self.args.push(arg.to_owned())
}
pub fn args<I>(mut self, args: I) -> Self
where
I: IntoIterator,
I::Item: AsRef<OsStr>,
{
args.into_iter().for_each(|it| self.arg_inner(it.as_ref()));
self
}
#[doc(hidden)]
pub fn __extend_arg(mut self, arg_fragment: impl AsRef<OsStr>) -> Cmd {
fn inner(sh: &mut Cmd, arg_fragment: &OsStr) {
match sh.args.last_mut() {
Some(last_arg) => last_arg.push(arg_fragment),
None => {
let mut inner = mem::take(&mut sh.prog).into_os_string();
inner.push(arg_fragment);
sh.prog = inner.into();
}
}
}
inner(&mut self, arg_fragment.as_ref());
self
}
pub fn env(mut self, key: impl AsRef<OsStr>, val: impl AsRef<OsStr>) -> Cmd {
fn inner(sh: &mut Cmd, key: &OsStr, val: &OsStr) {
Arc::make_mut(&mut sh.sh.env).insert(key.into(), val.into());
}
inner(&mut self, key.as_ref(), val.as_ref());
self
}
pub fn envs<I, K, V>(mut self, vars: I) -> Cmd
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
Arc::make_mut(&mut self.sh.env)
.extend(vars.into_iter().map(|(k, v)| (k.as_ref().into(), v.as_ref().into())));
self
}
pub fn env_remove(mut self, key: impl AsRef<OsStr>) -> Cmd {
fn inner(sh: &mut Cmd, key: &OsStr) {
Arc::make_mut(&mut sh.sh.env).remove(key);
}
inner(&mut self, key.as_ref());
self
}
pub fn env_clear(mut self) -> Cmd {
Arc::make_mut(&mut self.sh.env).clear();
self
}
pub fn stdin(mut self, stdin: impl AsRef<[u8]>) -> Cmd {
fn inner(sh: &mut Cmd, stdin: &[u8]) {
sh.stdin_contents = Some(stdin.to_vec());
}
inner(&mut self, stdin.as_ref());
self
}
pub fn ignore_status(mut self) -> Cmd {
self.set_ignore_status(true);
self
}
pub fn set_ignore_status(&mut self, yes: bool) {
self.ignore_status = yes;
}
pub fn timeout(mut self, timeout: Duration) -> Cmd {
self.set_timeout(Some(timeout));
self
}
pub fn set_timeout(&mut self, timeout: Option<Duration>) {
self.deadline = timeout.map(|it| Instant::now() + it)
}
pub fn deadline(mut self, deadline: Instant) -> Cmd {
self.set_deadline(Some(deadline));
self
}
pub fn set_deadline(&mut self, deadline: Option<Instant>) {
self.deadline = deadline;
}
pub fn secret(mut self) -> Cmd {
self.set_secret(true);
self
}
pub fn set_secret(&mut self, yes: bool) {
self.secret = yes;
}
pub fn run(&self) -> Result<()> {
let command = self.to_command();
let mut result = exec::exec(
command,
self.stdin_contents.as_deref(),
Some(STREAM_SUFFIX_SIZE),
Some(STREAM_SUFFIX_SIZE),
self.deadline,
);
self.check_exec_result(&mut result)?;
Ok(())
}
fn check_exec_result(&self, result: &mut exec::ExecResult) -> Result<()> {
if let Some(status) = result.status {
if !status.success() && !self.ignore_status {
return Err(Error::new_cmd(
self,
CmdErrorKind::Status(status),
mem::take(&mut result.stdout),
mem::take(&mut result.stderr),
));
}
}
if let Some(err) = result.error.take() {
if err.kind() == io::ErrorKind::TimedOut {
return Err(Error::new_cmd(
self,
CmdErrorKind::Timeout,
mem::take(&mut result.stdout),
mem::take(&mut result.stderr),
));
}
return Err(Error::new_cmd(
self,
CmdErrorKind::Io(err),
mem::take(&mut result.stdout),
mem::take(&mut result.stderr),
));
}
Ok(())
}
pub fn run_echo(&self) -> Result<()> {
let mut command = self.to_command();
command.stdin(Stdio::null());
command.stdout(Stdio::inherit());
command.stderr(Stdio::inherit());
eprintln!("$ {}", self);
let mut child = command
.spawn()
.map_err(|err| Error::new_cmd(self, CmdErrorKind::Io(err), Vec::new(), Vec::new()))?;
let status = exec::wait_deadline(&mut child, self.deadline)
.map_err(|err| Error::new_cmd(self, CmdErrorKind::Io(err), Vec::new(), Vec::new()))?;
if !status.success() {
return Err(Error::new_cmd(self, CmdErrorKind::Status(status), Vec::new(), Vec::new()));
}
Ok(())
}
pub fn run_interactive(&self) -> Result<()> {
let mut command = self.to_command();
command.stdin(Stdio::inherit());
command.stdout(Stdio::inherit());
command.stderr(Stdio::inherit());
eprintln!("$ {}", self);
let mut child = command
.spawn()
.map_err(|err| Error::new_cmd(self, CmdErrorKind::Io(err), Vec::new(), Vec::new()))?;
let status = exec::wait_deadline(&mut child, self.deadline)
.map_err(|err| Error::new_cmd(self, CmdErrorKind::Io(err), Vec::new(), Vec::new()))?;
if !status.success() {
return Err(Error::new_cmd(self, CmdErrorKind::Status(status), Vec::new(), Vec::new()));
}
Ok(())
}
pub fn read(&self) -> Result<String> {
let command = self.to_command();
let mut result = exec::exec(
command,
self.stdin_contents.as_deref(),
None,
Some(STREAM_SUFFIX_SIZE),
self.deadline,
);
self.check_exec_result(&mut result)?;
self.chomp(result.stdout)
}
pub fn read_stderr(&self) -> Result<String> {
let command = self.to_command();
let mut result = exec::exec(
command,
self.stdin_contents.as_deref(),
Some(STREAM_SUFFIX_SIZE),
None,
self.deadline,
);
self.check_exec_result(&mut result)?;
self.chomp(result.stderr)
}
fn chomp(&self, stream: Vec<u8>) -> Result<String> {
let mut text = String::from_utf8(stream)
.map_err(|err| Error::new_cmd(self, CmdErrorKind::Utf8(err), Vec::new(), Vec::new()))?;
if text.ends_with('\n') && !text[0..text.len() - 1].contains('\n') {
text.pop();
if text.ends_with('\r') {
text.pop();
}
}
Ok(text)
}
pub fn output(&self) -> Result<Output> {
let mut command = self.to_command();
command.stdin(Stdio::null());
command.stdout(Stdio::piped());
command.stderr(Stdio::piped());
let mut result =
exec::exec(command, self.stdin_contents.as_deref(), None, None, self.deadline);
self.check_exec_result(&mut result)?;
Ok(Output {
status: result.status.take().unwrap(),
stdout: result.stdout,
stderr: result.stderr,
})
}
pub fn to_command(&self) -> Command {
let mut result = Command::new(&self.prog);
result.current_dir(&self.sh.cwd);
result.args(&self.args);
result.envs(&*self.sh.env);
result
}
}
#[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)
}