1use std::cell::RefCell;
5use std::collections::HashMap;
6use std::future::Future;
7use std::path::{Path, PathBuf};
8use std::sync::RwLock;
9use std::time::Duration;
10
11use once_cell::sync::Lazy;
12
13use super::error::ZxError;
14use super::kill::SIGTERM;
15use super::process::ProcessPromise;
16use super::util::{
17 build_cmd, parse_bool, parse_duration, quote, quote_powershell, to_camel_case, QuoteFn, ZxArg,
18};
19pub use crate::local_bin::PreferLocal;
20
21pub const BASH_PREFIX: &str = "set -euo pipefail;";
23pub const POWERSHELL_POSTFIX: &str = "; exit $LastExitCode";
25
26#[derive(Debug, Clone)]
28pub struct Options {
29 pub cwd: Option<PathBuf>,
31 pub env: Option<HashMap<String, String>>,
33 pub shell: Option<String>,
35 pub prefix: String,
37 pub postfix: String,
39 pub verbose: bool,
41 pub quiet: bool,
43 pub nothrow: bool,
45 pub timeout: Option<Duration>,
47 pub timeout_signal: String,
49 pub kill_signal: String,
51 pub prefer_local: PreferLocal,
53 pub input: Option<Vec<u8>>,
55 pub quote: QuoteFn,
57}
58
59impl Options {
60 pub fn builtin() -> Self {
63 let mut opts = Options {
64 cwd: None,
65 env: None,
66 shell: None,
67 prefix: String::new(),
68 postfix: String::new(),
69 verbose: false,
70 quiet: false,
71 nothrow: false,
72 timeout: None,
73 timeout_signal: SIGTERM.to_string(),
74 kill_signal: SIGTERM.to_string(),
75 prefer_local: PreferLocal::Off,
76 input: None,
77 quote,
78 };
79 opts.use_bash();
80 opts
81 }
82
83 pub fn use_bash(&mut self) {
86 let bash = find_executable("bash");
87 self.prefix = if bash.is_some() {
88 BASH_PREFIX.to_string()
89 } else {
90 String::new()
91 };
92 self.shell = bash.or_else(|| find_executable("sh"));
93 self.postfix = String::new();
94 self.quote = quote;
95 }
96
97 pub fn use_pwsh(&mut self) {
99 self.use_powershell_named("pwsh");
100 }
101
102 pub fn use_powershell(&mut self) {
104 self.use_powershell_named("powershell.exe");
105 }
106
107 fn use_powershell_named(&mut self, name: &str) {
108 self.shell = Some(find_executable(name).unwrap_or_else(|| name.to_string()));
109 self.prefix = String::new();
110 self.postfix = POWERSHELL_POSTFIX.to_string();
111 self.quote = quote_powershell;
112 }
113
114 pub fn resolve_env<I, K, V>(&mut self, prefix: &str, env: I)
120 where
121 I: IntoIterator<Item = (K, V)>,
122 K: AsRef<str>,
123 V: AsRef<str>,
124 {
125 for (key, value) in env {
126 let (key, value) = (key.as_ref(), value.as_ref());
127 let Some(name) = key.strip_prefix(prefix) else {
128 continue;
129 };
130 if value.is_empty() {
131 continue;
132 }
133 let flag = || parse_bool(value).unwrap_or(false);
134 match to_camel_case(name).as_str() {
135 "cwd" => self.cwd = Some(PathBuf::from(value)),
136 "preferLocal" => {
137 self.prefer_local = match parse_bool(value) {
138 Some(true) => PreferLocal::Cwd,
139 Some(false) => PreferLocal::Off,
140 None => PreferLocal::Dirs(vec![PathBuf::from(value)]),
141 }
142 }
143 "verbose" => self.verbose = flag(),
144 "quiet" => self.quiet = flag(),
145 "timeout" => self.timeout = parse_duration(value).ok(),
146 "timeoutSignal" => self.timeout_signal = value.to_string(),
147 "killSignal" => self.kill_signal = value.to_string(),
148 "prefix" => self.prefix = value.to_string(),
149 "postfix" => self.postfix = value.to_string(),
150 "shell" => self.shell = Some(value.to_string()),
151 _ => {}
152 }
153 }
154 }
155
156 pub fn effective_cwd(&self) -> PathBuf {
158 match &self.cwd {
159 Some(dir) if dir.is_absolute() => dir.clone(),
160 Some(dir) => std::env::current_dir().unwrap_or_default().join(dir),
161 None => std::env::current_dir().unwrap_or_default(),
162 }
163 }
164}
165
166impl Default for Options {
167 fn default() -> Self {
168 Self::builtin()
169 }
170}
171
172pub fn find_executable(name: &str) -> Option<String> {
174 which::which(name)
175 .ok()
176 .map(|p| p.to_string_lossy().into_owned())
177}
178
179static GLOBAL: Lazy<RwLock<Options>> = Lazy::new(|| {
180 let mut opts = Options::builtin();
181 opts.resolve_env("ZX_", std::env::vars());
182 RwLock::new(opts)
183});
184
185tokio::task_local! {
186 static SCOPE: RefCell<Options>;
187}
188
189fn in_scope() -> bool {
190 SCOPE.try_with(|_| ()).is_ok()
191}
192
193pub fn current_options() -> Options {
196 SCOPE
197 .try_with(|scope| scope.borrow().clone())
198 .unwrap_or_else(|_| GLOBAL.read().unwrap_or_else(|e| e.into_inner()).clone())
199}
200
201pub fn configure<R>(update: impl FnOnce(&mut Options) -> R) -> R {
205 if in_scope() {
206 SCOPE.with(|scope| update(&mut scope.borrow_mut()))
207 } else {
208 let mut global = GLOBAL.write().unwrap_or_else(|e| e.into_inner());
209 update(&mut global)
210 }
211}
212
213pub async fn within<F: Future>(fut: F) -> F::Output {
219 SCOPE.scope(RefCell::new(current_options()), fut).await
220}
221
222pub fn within_sync<R>(f: impl FnOnce() -> R) -> R {
224 SCOPE.sync_scope(RefCell::new(current_options()), f)
225}
226
227pub fn use_bash() {
229 configure(Options::use_bash);
230}
231
232pub fn use_pwsh() {
234 configure(Options::use_pwsh);
235}
236
237pub fn use_powershell() {
239 configure(Options::use_powershell);
240}
241
242pub fn cd<P: AsRef<Path>>(dir: P) -> Result<PathBuf, ZxError> {
249 let dir = dir.as_ref();
250 let base = current_options().effective_cwd();
251 let target = base.join(dir);
252 let resolved = std::fs::canonicalize(&target).map_err(|e| {
253 ZxError::new(format!(
254 "ENOENT: {}, chdir '{}' -> '{}'",
255 e,
256 base.display(),
257 dir.display()
258 ))
259 })?;
260 if !resolved.is_dir() {
261 return Err(ZxError::new(format!(
262 "ENOTDIR: not a directory, chdir '{}'",
263 dir.display()
264 )));
265 }
266 let opts = configure(|opts| {
267 opts.cwd = Some(resolved.clone());
268 opts.clone()
269 });
270 let dir = resolved.display().to_string();
271 super::log::log(
272 &super::log::LogEntry::Cd { dir },
273 opts.verbose && !opts.quiet,
274 );
275 Ok(resolved)
276}
277
278#[derive(Debug, Clone, Default)]
283pub struct Shell {
284 opts: Options,
285}
286
287macro_rules! setter {
288 ($(#[$doc:meta])* $name:ident: $ty:ty => |$o:ident, $v:ident| $body:expr) => {
289 $(#[$doc])*
290 pub fn $name(mut self, $v: $ty) -> Self {
291 let $o = &mut self.opts;
292 $body;
293 self
294 }
295 };
296}
297
298impl Shell {
299 pub fn new() -> Self {
301 Self {
302 opts: current_options(),
303 }
304 }
305
306 pub fn with_options(opts: Options) -> Self {
308 Self { opts }
309 }
310
311 pub fn options(&self) -> &Options {
313 &self.opts
314 }
315
316 pub fn options_mut(&mut self) -> &mut Options {
318 &mut self.opts
319 }
320
321 setter!(cwd: impl AsRef<Path> => |o, v| o.cwd = Some(v.as_ref().to_path_buf()));
323 setter!(env: HashMap<String, String> => |o, v| o.env = Some(v));
325 setter!(shell: impl Into<String> => |o, v| o.shell = Some(v.into()));
327 setter!(prefix: impl Into<String> => |o, v| o.prefix = v.into());
329 setter!(postfix: impl Into<String> => |o, v| o.postfix = v.into());
331 setter!(verbose: bool => |o, v| o.verbose = v);
333 setter!(quiet: bool => |o, v| o.quiet = v);
335 setter!(nothrow: bool => |o, v| o.nothrow = v);
337 setter!(timeout: Duration => |o, v| o.timeout = Some(v));
339 setter!(timeout_signal: impl Into<String> => |o, v| o.timeout_signal = v.into());
341 setter!(kill_signal: impl Into<String> => |o, v| o.kill_signal = v.into());
343 setter!(prefer_local: bool => |o, v| o.prefer_local = if v { PreferLocal::Cwd } else { PreferLocal::Off });
345 setter!(prefer_local_dirs: Vec<PathBuf> => |o, v| o.prefer_local = PreferLocal::Dirs(v));
347 setter!(input: impl Into<Vec<u8>> => |o, v| o.input = Some(v.into()));
349 setter!(quote_with: QuoteFn => |o, v| o.quote = v);
351
352 pub fn env_var(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
355 self.opts
356 .env
357 .get_or_insert_with(|| std::env::vars().collect())
358 .insert(key.into(), value.into());
359 self
360 }
361
362 pub fn use_bash(mut self) -> Self {
364 self.opts.use_bash();
365 self
366 }
367
368 pub fn use_pwsh(mut self) -> Self {
370 self.opts.use_pwsh();
371 self
372 }
373
374 pub fn use_powershell(mut self) -> Self {
376 self.opts.use_powershell();
377 self
378 }
379
380 pub fn cmd<S: AsRef<str>>(&self, pieces: &[S], args: &[ZxArg]) -> ProcessPromise {
383 let pieces: Vec<&str> = pieces.iter().map(|p| p.as_ref()).collect();
384 match build_cmd(self.opts.quote, &pieces, args) {
385 Ok(cmd) => ProcessPromise::new(self.opts.clone(), cmd),
386 Err(err) => ProcessPromise::failed(self.opts.clone(), err),
387 }
388 }
389
390 pub fn command(&self, cmd: impl Into<String>) -> ProcessPromise {
392 ProcessPromise::new(self.opts.clone(), cmd.into())
393 }
394}