//! A bad shell -- small cross platform module for writing glue code use std::{ cell::RefCell, env, ffi::OsString, io::{self, Write}, path::{Path, PathBuf}, process::{Command, Stdio}, }; use anyhow::{bail, Context, Result}; pub use fs_err as fs2; #[macro_export] macro_rules! run { ($($expr:expr),*) => { run!($($expr),*; echo = true) }; ($($expr:expr),* ; echo = $echo:expr) => { $crate::not_bash::run_process(format!($($expr),*), $echo, None) }; ($($expr:expr),* ; <$stdin:expr) => { $crate::not_bash::run_process(format!($($expr),*), false, Some($stdin)) }; } pub use crate::run; pub struct Pushd { _p: (), } pub fn pushd(path: impl Into) -> Pushd { Env::with(|env| env.pushd(path.into())); Pushd { _p: () } } impl Drop for Pushd { fn drop(&mut self) { Env::with(|env| env.popd()) } } pub struct Pushenv { _p: (), } pub fn pushenv(var: &str, value: &str) -> Pushenv { Env::with(|env| env.pushenv(var.into(), value.into())); Pushenv { _p: () } } impl Drop for Pushenv { fn drop(&mut self) { Env::with(|env| env.popenv()) } } pub fn rm_rf(path: impl AsRef) -> io::Result<()> { let path = path.as_ref(); if !path.exists() { return Ok(()); } if path.is_file() { fs2::remove_file(path) } else { fs2::remove_dir_all(path) } } #[doc(hidden)] pub fn run_process(cmd: String, echo: bool, stdin: Option<&[u8]>) -> Result { run_process_inner(&cmd, echo, stdin).with_context(|| format!("process `{}` failed", cmd)) } pub fn date_iso() -> Result { run!("date --iso --utc") } fn run_process_inner(cmd: &str, echo: bool, stdin: Option<&[u8]>) -> Result { let mut args = shelx(cmd); let binary = args.remove(0); let current_dir = Env::with(|it| it.cwd().to_path_buf()); if echo { println!("> {}", cmd) } let mut command = Command::new(binary); command.args(args).current_dir(current_dir).stderr(Stdio::inherit()); let output = match stdin { None => command.stdin(Stdio::null()).output(), Some(stdin) => { command.stdin(Stdio::piped()).stdout(Stdio::piped()); let mut process = command.spawn()?; process.stdin.take().unwrap().write_all(stdin)?; process.wait_with_output() } }?; let stdout = String::from_utf8(output.stdout)?; if echo { print!("{}", stdout) } if !output.status.success() { bail!("{}", output.status) } Ok(stdout.trim().to_string()) } // FIXME: some real shell lexing here fn shelx(cmd: &str) -> Vec { let mut res = Vec::new(); for (string_piece, in_quotes) in cmd.split('\'').zip([false, true].iter().copied().cycle()) { if in_quotes { res.push(string_piece.to_string()) } else { if !string_piece.is_empty() { res.extend(string_piece.split_ascii_whitespace().map(|it| it.to_string())) } } } res } struct Env { pushd_stack: Vec, pushenv_stack: Vec<(OsString, Option)>, } impl Env { fn with T, T>(f: F) -> T { thread_local! { static ENV: RefCell = RefCell::new(Env { pushd_stack: vec![env::current_dir().unwrap()], pushenv_stack: vec![], }); } ENV.with(|it| f(&mut *it.borrow_mut())) } fn pushd(&mut self, dir: PathBuf) { let dir = self.cwd().join(dir); self.pushd_stack.push(dir); env::set_current_dir(self.cwd()) .unwrap_or_else(|err| panic!("Failed to set cwd to {}: {}", self.cwd().display(), err)); } fn popd(&mut self) { self.pushd_stack.pop().unwrap(); env::set_current_dir(self.cwd()).unwrap(); } fn pushenv(&mut self, var: OsString, value: OsString) { self.pushenv_stack.push((var.clone(), env::var_os(&var))); env::set_var(var, value) } fn popenv(&mut self) { let (var, value) = self.pushenv_stack.pop().unwrap(); match value { None => env::remove_var(var), Some(value) => env::set_var(var, value), } } fn cwd(&self) -> &Path { self.pushd_stack.last().unwrap() } }