use std::cell::RefCell;
use std::collections::HashMap;
use std::future::Future;
use std::path::{Path, PathBuf};
use std::sync::RwLock;
use std::time::Duration;
use once_cell::sync::Lazy;
use super::error::ZxError;
use super::kill::SIGTERM;
use super::process::ProcessPromise;
use super::util::{
build_cmd, parse_bool, parse_duration, quote, quote_powershell, to_camel_case, QuoteFn, ZxArg,
};
pub use crate::local_bin::PreferLocal;
pub const BASH_PREFIX: &str = "set -euo pipefail;";
pub const POWERSHELL_POSTFIX: &str = "; exit $LastExitCode";
#[derive(Debug, Clone)]
pub struct Options {
pub cwd: Option<PathBuf>,
pub env: Option<HashMap<String, String>>,
pub shell: Option<String>,
pub prefix: String,
pub postfix: String,
pub verbose: bool,
pub quiet: bool,
pub nothrow: bool,
pub timeout: Option<Duration>,
pub timeout_signal: String,
pub kill_signal: String,
pub prefer_local: PreferLocal,
pub input: Option<Vec<u8>>,
pub quote: QuoteFn,
}
impl Options {
pub fn builtin() -> Self {
let mut opts = Options {
cwd: None,
env: None,
shell: None,
prefix: String::new(),
postfix: String::new(),
verbose: false,
quiet: false,
nothrow: false,
timeout: None,
timeout_signal: SIGTERM.to_string(),
kill_signal: SIGTERM.to_string(),
prefer_local: PreferLocal::Off,
input: None,
quote,
};
opts.use_bash();
opts
}
pub fn use_bash(&mut self) {
let bash = find_executable("bash");
self.prefix = if bash.is_some() {
BASH_PREFIX.to_string()
} else {
String::new()
};
self.shell = bash.or_else(|| find_executable("sh"));
self.postfix = String::new();
self.quote = quote;
}
pub fn use_pwsh(&mut self) {
self.use_powershell_named("pwsh");
}
pub fn use_powershell(&mut self) {
self.use_powershell_named("powershell.exe");
}
fn use_powershell_named(&mut self, name: &str) {
self.shell = Some(find_executable(name).unwrap_or_else(|| name.to_string()));
self.prefix = String::new();
self.postfix = POWERSHELL_POSTFIX.to_string();
self.quote = quote_powershell;
}
pub fn resolve_env<I, K, V>(&mut self, prefix: &str, env: I)
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<str>,
V: AsRef<str>,
{
for (key, value) in env {
let (key, value) = (key.as_ref(), value.as_ref());
let Some(name) = key.strip_prefix(prefix) else {
continue;
};
if value.is_empty() {
continue;
}
let flag = || parse_bool(value).unwrap_or(false);
match to_camel_case(name).as_str() {
"cwd" => self.cwd = Some(PathBuf::from(value)),
"preferLocal" => {
self.prefer_local = match parse_bool(value) {
Some(true) => PreferLocal::Cwd,
Some(false) => PreferLocal::Off,
None => PreferLocal::Dirs(vec![PathBuf::from(value)]),
}
}
"verbose" => self.verbose = flag(),
"quiet" => self.quiet = flag(),
"timeout" => self.timeout = parse_duration(value).ok(),
"timeoutSignal" => self.timeout_signal = value.to_string(),
"killSignal" => self.kill_signal = value.to_string(),
"prefix" => self.prefix = value.to_string(),
"postfix" => self.postfix = value.to_string(),
"shell" => self.shell = Some(value.to_string()),
_ => {}
}
}
}
pub fn effective_cwd(&self) -> PathBuf {
match &self.cwd {
Some(dir) if dir.is_absolute() => dir.clone(),
Some(dir) => std::env::current_dir().unwrap_or_default().join(dir),
None => std::env::current_dir().unwrap_or_default(),
}
}
}
impl Default for Options {
fn default() -> Self {
Self::builtin()
}
}
pub fn find_executable(name: &str) -> Option<String> {
which::which(name)
.ok()
.map(|p| p.to_string_lossy().into_owned())
}
static GLOBAL: Lazy<RwLock<Options>> = Lazy::new(|| {
let mut opts = Options::builtin();
opts.resolve_env("ZX_", std::env::vars());
RwLock::new(opts)
});
tokio::task_local! {
static SCOPE: RefCell<Options>;
}
fn in_scope() -> bool {
SCOPE.try_with(|_| ()).is_ok()
}
pub fn current_options() -> Options {
SCOPE
.try_with(|scope| scope.borrow().clone())
.unwrap_or_else(|_| GLOBAL.read().unwrap_or_else(|e| e.into_inner()).clone())
}
pub fn configure<R>(update: impl FnOnce(&mut Options) -> R) -> R {
if in_scope() {
SCOPE.with(|scope| update(&mut scope.borrow_mut()))
} else {
let mut global = GLOBAL.write().unwrap_or_else(|e| e.into_inner());
update(&mut global)
}
}
pub async fn within<F: Future>(fut: F) -> F::Output {
SCOPE.scope(RefCell::new(current_options()), fut).await
}
pub fn within_sync<R>(f: impl FnOnce() -> R) -> R {
SCOPE.sync_scope(RefCell::new(current_options()), f)
}
pub fn use_bash() {
configure(Options::use_bash);
}
pub fn use_pwsh() {
configure(Options::use_pwsh);
}
pub fn use_powershell() {
configure(Options::use_powershell);
}
pub fn cd<P: AsRef<Path>>(dir: P) -> Result<PathBuf, ZxError> {
let dir = dir.as_ref();
let base = current_options().effective_cwd();
let target = base.join(dir);
let resolved = std::fs::canonicalize(&target).map_err(|e| {
ZxError::new(format!(
"ENOENT: {}, chdir '{}' -> '{}'",
e,
base.display(),
dir.display()
))
})?;
if !resolved.is_dir() {
return Err(ZxError::new(format!(
"ENOTDIR: not a directory, chdir '{}'",
dir.display()
)));
}
let opts = configure(|opts| {
opts.cwd = Some(resolved.clone());
opts.clone()
});
let dir = resolved.display().to_string();
super::log::log(
&super::log::LogEntry::Cd { dir },
opts.verbose && !opts.quiet,
);
Ok(resolved)
}
#[derive(Debug, Clone, Default)]
pub struct Shell {
opts: Options,
}
macro_rules! setter {
($(#[$doc:meta])* $name:ident: $ty:ty => |$o:ident, $v:ident| $body:expr) => {
$(#[$doc])*
pub fn $name(mut self, $v: $ty) -> Self {
let $o = &mut self.opts;
$body;
self
}
};
}
impl Shell {
pub fn new() -> Self {
Self {
opts: current_options(),
}
}
pub fn with_options(opts: Options) -> Self {
Self { opts }
}
pub fn options(&self) -> &Options {
&self.opts
}
pub fn options_mut(&mut self) -> &mut Options {
&mut self.opts
}
setter!( cwd: impl AsRef<Path> => |o, v| o.cwd = Some(v.as_ref().to_path_buf()));
setter!( env: HashMap<String, String> => |o, v| o.env = Some(v));
setter!( shell: impl Into<String> => |o, v| o.shell = Some(v.into()));
setter!( prefix: impl Into<String> => |o, v| o.prefix = v.into());
setter!( postfix: impl Into<String> => |o, v| o.postfix = v.into());
setter!( verbose: bool => |o, v| o.verbose = v);
setter!( quiet: bool => |o, v| o.quiet = v);
setter!( nothrow: bool => |o, v| o.nothrow = v);
setter!( timeout: Duration => |o, v| o.timeout = Some(v));
setter!( timeout_signal: impl Into<String> => |o, v| o.timeout_signal = v.into());
setter!( kill_signal: impl Into<String> => |o, v| o.kill_signal = v.into());
setter!( prefer_local: bool => |o, v| o.prefer_local = if v { PreferLocal::Cwd } else { PreferLocal::Off });
setter!( prefer_local_dirs: Vec<PathBuf> => |o, v| o.prefer_local = PreferLocal::Dirs(v));
setter!( input: impl Into<Vec<u8>> => |o, v| o.input = Some(v.into()));
setter!( quote_with: QuoteFn => |o, v| o.quote = v);
pub fn env_var(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.opts
.env
.get_or_insert_with(|| std::env::vars().collect())
.insert(key.into(), value.into());
self
}
pub fn use_bash(mut self) -> Self {
self.opts.use_bash();
self
}
pub fn use_pwsh(mut self) -> Self {
self.opts.use_pwsh();
self
}
pub fn use_powershell(mut self) -> Self {
self.opts.use_powershell();
self
}
pub fn cmd<S: AsRef<str>>(&self, pieces: &[S], args: &[ZxArg]) -> ProcessPromise {
let pieces: Vec<&str> = pieces.iter().map(|p| p.as_ref()).collect();
match build_cmd(self.opts.quote, &pieces, args) {
Ok(cmd) => ProcessPromise::new(self.opts.clone(), cmd),
Err(err) => ProcessPromise::failed(self.opts.clone(), err),
}
}
pub fn command(&self, cmd: impl Into<String>) -> ProcessPromise {
ProcessPromise::new(self.opts.clone(), cmd.into())
}
}