1pub(crate) mod builtin;
26pub(crate) mod builtins;
27pub(crate) mod env;
28pub(crate) mod errno;
29pub(crate) mod io;
30pub(crate) mod node_path;
31pub(crate) mod subprocess;
32
33use std::collections::HashMap;
34use std::fmt;
35use std::path::PathBuf;
36use std::sync::{Arc, Mutex};
37
38mod braces;
39mod expansion;
40pub(crate) mod glob;
41mod interpreter;
42mod lexer;
43mod parser;
44mod template;
45
46pub use braces::BraceError;
47
48pub fn braces(pattern: &str) -> Result<Vec<String>, BraceError> {
51 braces::braces(pattern)
52}
53
54pub fn escape(s: &str) -> String {
57 template::shell_escape(s)
58}
59
60#[derive(Clone, Debug)]
63pub struct OutBuffer(Arc<Mutex<Vec<u8>>>);
64
65impl OutBuffer {
66 pub fn new(size: usize) -> Self {
68 Self(Arc::new(Mutex::new(vec![0; size])))
69 }
70
71 pub fn contents(&self) -> Vec<u8> {
73 self.0.lock().map(|b| b.clone()).unwrap_or_default()
74 }
75
76 pub fn with<R>(&self, f: impl FnOnce(&mut [u8]) -> R) -> R {
78 let mut guard = self.0.lock().unwrap_or_else(|e| e.into_inner());
79 f(guard.as_mut_slice())
80 }
81
82 pub fn ptr_eq(&self, other: &Self) -> bool {
84 Arc::ptr_eq(&self.0, &other.0)
85 }
86}
87
88#[derive(Clone, Debug)]
91pub enum ShellValue {
92 Str(String),
94 Raw(String),
96 Number(f64),
98 BigInt(String),
100 Bool(bool),
101 Null,
102 Undefined,
103 Array(Vec<ShellValue>),
105 Bytes(Vec<u8>),
107 OutBuffer(OutBuffer),
109}
110
111impl From<&str> for ShellValue {
112 fn from(s: &str) -> Self {
113 Self::Str(s.to_string())
114 }
115}
116
117impl From<String> for ShellValue {
118 fn from(s: String) -> Self {
119 Self::Str(s)
120 }
121}
122
123impl From<&String> for ShellValue {
124 fn from(s: &String) -> Self {
125 Self::Str(s.clone())
126 }
127}
128
129impl From<&std::path::Path> for ShellValue {
130 fn from(p: &std::path::Path) -> Self {
131 Self::Str(p.to_string_lossy().into_owned())
132 }
133}
134
135impl From<PathBuf> for ShellValue {
136 fn from(p: PathBuf) -> Self {
137 Self::Str(p.to_string_lossy().into_owned())
138 }
139}
140
141impl From<bool> for ShellValue {
142 fn from(b: bool) -> Self {
143 Self::Bool(b)
144 }
145}
146
147macro_rules! number_from {
148 ($($t:ty),*) => {
149 $(impl From<$t> for ShellValue {
150 fn from(n: $t) -> Self {
151 Self::Number(n as f64)
152 }
153 })*
154 };
155}
156number_from!(i8, i16, i32, i64, u8, u16, u32, u64, usize, isize, f32, f64);
157
158impl<T: Into<ShellValue>> From<Vec<T>> for ShellValue {
159 fn from(v: Vec<T>) -> Self {
160 Self::Array(v.into_iter().map(Into::into).collect())
161 }
162}
163
164impl From<OutBuffer> for ShellValue {
165 fn from(b: OutBuffer) -> Self {
166 Self::OutBuffer(b)
167 }
168}
169
170#[derive(Clone, Debug, Default, PartialEq, Eq)]
172pub struct ShellOutput {
173 pub stdout: Vec<u8>,
174 pub stderr: Vec<u8>,
175 pub exit_code: i32,
176}
177
178impl ShellOutput {
179 pub fn text(&self) -> String {
181 String::from_utf8_lossy(&self.stdout).into_owned()
182 }
183
184 pub fn json(&self) -> Result<serde_json::Value, serde_json::Error> {
186 serde_json::from_slice(&self.stdout)
187 }
188
189 pub fn lines(&self) -> Vec<String> {
191 self.text().split('\n').map(str::to_string).collect()
192 }
193}
194
195#[derive(Clone, Copy, Debug, PartialEq, Eq)]
197pub enum ShellErrorKind {
198 Parse,
201 Exit,
204 System,
207}
208
209#[derive(Clone, Debug)]
211pub struct ShellError {
212 pub kind: ShellErrorKind,
213 pub message: String,
215 pub output: Option<ShellOutput>,
217}
218
219impl ShellError {
220 pub fn parse(message: impl Into<String>) -> Self {
221 Self {
222 kind: ShellErrorKind::Parse,
223 message: message.into(),
224 output: None,
225 }
226 }
227
228 pub fn system(message: impl Into<String>) -> Self {
229 Self {
230 kind: ShellErrorKind::System,
231 message: message.into(),
232 output: None,
233 }
234 }
235
236 pub fn exit(output: ShellOutput) -> Self {
237 Self {
238 kind: ShellErrorKind::Exit,
239 message: format!("Failed with exit code {}", output.exit_code),
240 output: Some(output),
241 }
242 }
243
244 pub fn exit_code(&self) -> Option<i32> {
245 self.output.as_ref().map(|o| o.exit_code)
246 }
247}
248
249impl fmt::Display for ShellError {
250 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251 f.write_str(&self.message)
252 }
253}
254
255impl std::error::Error for ShellError {}
256
257#[derive(Clone, Debug)]
260pub struct Shell {
261 cwd: Option<PathBuf>,
262 env: Option<HashMap<String, String>>,
263 prefer_local: crate::PreferLocal,
264 throws: bool,
265}
266
267impl Default for Shell {
268 fn default() -> Self {
269 Self::new()
270 }
271}
272
273impl Shell {
274 pub fn new() -> Self {
275 Self {
276 cwd: None,
277 env: None,
278 prefer_local: crate::PreferLocal::Off,
279 throws: true,
280 }
281 }
282
283 pub fn cwd(&mut self, cwd: Option<impl Into<PathBuf>>) -> &mut Self {
285 self.cwd = cwd.map(Into::into);
286 self
287 }
288
289 pub fn env(&mut self, env: Option<HashMap<String, String>>) -> &mut Self {
291 self.env = env;
292 self
293 }
294
295 pub fn prefer_local(&mut self, preference: crate::PreferLocal) -> &mut Self {
297 self.prefer_local = preference;
298 self
299 }
300
301 pub fn nothrow(&mut self) -> &mut Self {
302 self.throws = false;
303 self
304 }
305
306 pub fn throws(&mut self, throws: bool) -> &mut Self {
307 self.throws = throws;
308 self
309 }
310
311 pub fn command(
314 &self,
315 strings: &[&str],
316 values: Vec<ShellValue>,
317 ) -> Result<ShellCommand, ShellError> {
318 let mut cmd = ShellCommand::parse(strings, values)?;
319 cmd.throws = self.throws;
320 if let Some(cwd) = &self.cwd {
321 cmd = cmd.cwd(cwd.clone());
322 }
323 if let Some(env) = &self.env {
324 cmd = cmd.env(env.clone());
325 }
326 cmd = cmd.prefer_local(self.prefer_local.clone());
327 Ok(cmd)
328 }
329}
330
331pub fn shell(strings: &[&str], values: Vec<ShellValue>) -> Result<ShellCommand, ShellError> {
333 Shell::new().command(strings, values)
334}
335
336pub struct ShellCommand {
339 script: ParsedScript,
340 cwd: Option<PathBuf>,
341 env: HashMap<String, String>,
342 prefer_local: crate::PreferLocal,
343 quiet: bool,
344 throws: bool,
345}
346
347impl fmt::Debug for ShellCommand {
350 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
351 let mut env: Vec<&str> = self.env.keys().map(String::as_str).collect();
352 env.sort_unstable();
353 f.debug_struct("ShellCommand")
354 .field("script", &self.script)
355 .field("cwd", &self.cwd)
356 .field("env", &env)
357 .field("prefer_local", &self.prefer_local)
358 .field("quiet", &self.quiet)
359 .field("throws", &self.throws)
360 .finish()
361 }
362}
363
364#[derive(Debug)]
366pub(crate) struct ParsedScript {
367 pub(crate) ast: parser::Script,
368 #[cfg_attr(not(test), allow(dead_code))]
371 pub(crate) jsstrings: Vec<String>,
372 pub(crate) jsobjs: Vec<ShellValue>,
374}
375
376impl ShellCommand {
377 fn parse(strings: &[&str], values: Vec<ShellValue>) -> Result<Self, ShellError> {
378 let src = template::build_shell_source(strings, values).map_err(ShellError::parse)?;
379 let ast = parser::parse(&src.script, &src.jsstrings, src.jsobjs.len())
380 .map_err(ShellError::parse)?;
381 Ok(Self {
382 script: ParsedScript {
383 ast,
384 jsstrings: src.jsstrings,
385 jsobjs: src.jsobjs,
386 },
387 cwd: None,
388 env: std::env::vars().collect(),
389 prefer_local: crate::PreferLocal::Off,
390 quiet: false,
391 throws: true,
392 })
393 }
394
395 pub fn cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
397 let cwd = cwd.into();
398 let s = cwd.to_string_lossy();
399 self.cwd = if s.is_empty() || s == "." || s == "./" {
400 std::env::current_dir().ok()
401 } else {
402 Some(cwd)
403 };
404 self
405 }
406
407 pub fn env(mut self, env: HashMap<String, String>) -> Self {
409 self.env = env;
410 self
411 }
412
413 pub fn prefer_local(mut self, preference: crate::PreferLocal) -> Self {
415 self.prefer_local = preference;
416 self
417 }
418
419 pub fn quiet(mut self) -> Self {
421 self.quiet = true;
422 self
423 }
424
425 pub fn nothrow(mut self) -> Self {
427 self.throws = false;
428 self
429 }
430
431 pub fn throws(mut self, throws: bool) -> Self {
432 self.throws = throws;
433 self
434 }
435
436 pub async fn run(mut self) -> Result<ShellOutput, ShellError> {
438 let cwd = self
439 .cwd
440 .clone()
441 .or_else(|| std::env::current_dir().ok())
442 .unwrap_or_else(|| PathBuf::from("."));
443 if let Some((key, path)) =
444 crate::local_bin::preferred_path(Some(&self.env), &cwd, &self.prefer_local)
445 {
446 self.env.insert(key, path);
447 }
448 let mut env: Vec<(String, String)> = self.env.into_iter().collect();
450 env.sort_unstable_by(|a, b| a.0.cmp(&b.0));
451 let (interp, mut root) = interpreter::Interpreter::new(interpreter::InterpreterOptions {
452 jsobjs: self.script.jsobjs,
453 env: env.into_iter().collect(),
454 cwd: self.cwd.map(|p| p.to_string_lossy().into_owned()),
455 quiet: self.quiet,
456 argv: std::env::args().collect(),
457 })?;
458 let out = interp.run(&self.script.ast, &mut root).await?;
459 let output = ShellOutput {
460 stdout: out.stdout,
461 stderr: out.stderr,
462 exit_code: out.exit_code,
463 };
464 if self.throws && output.exit_code != 0 {
465 return Err(ShellError::exit(output));
466 }
467 Ok(output)
468 }
469
470 pub async fn text(self) -> Result<String, ShellError> {
472 Ok(self.quiet().run().await?.text())
473 }
474}
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479
480 fn parse_error(strings: &[&str], values: Vec<ShellValue>) -> ShellError {
481 match shell(strings, values) {
482 Ok(_) => panic!("expected a parse error for {strings:?}"),
483 Err(e) => e,
484 }
485 }
486
487 #[test]
488 fn frontend_errors_are_parse_errors_with_the_js_message() {
489 let e = parse_error(&["echo ("], vec![]);
490 assert_eq!(e.kind, ShellErrorKind::Parse);
491 assert_eq!(e.message, "Unclosed subshell");
492 let e = parse_error(&["echo $(echo"], vec![]);
493 assert_eq!(e.message, "Unclosed command substitution");
494 let e = parse_error(&["echo hi &"], vec![]);
495 assert_eq!(
496 e.message,
497 "Background commands \"&\" are not supported yet."
498 );
499 let e = parse_error(&["echo ", ""], vec![ShellValue::Str("a\0b".into())]);
500 assert_eq!(e.kind, ShellErrorKind::Parse);
501 }
502
503 #[test]
504 fn debug_shows_env_names_but_not_values() {
505 let env = HashMap::from([("TOKEN".to_string(), "s3cret".to_string())]);
506 let cmd = shell(&["echo hi"], vec![]).unwrap().env(env);
507 let debug = format!("{cmd:?}");
508 assert!(debug.contains("TOKEN"), "{debug}");
509 assert!(!debug.contains("s3cret"), "{debug}");
510 }
511
512 #[test]
513 fn parse_keeps_interpolated_strings_and_objects() {
514 let cmd = shell(
515 &["cat ", " < ", ""],
516 vec![ShellValue::from("a b"), ShellValue::Bytes(b"x".to_vec())],
517 )
518 .unwrap();
519 assert_eq!(cmd.script.jsstrings, ["a b"]);
520 assert_eq!(cmd.script.jsobjs.len(), 1);
521 assert_eq!(cmd.script.ast.stmts.len(), 1);
522 }
523
524 #[test]
525 fn braces_and_escape() {
526 assert_eq!(braces("x{a,b}").unwrap(), ["xa", "xb"]);
527 assert_eq!(
528 braces(&"{a,b}".repeat(17)).unwrap_err(),
529 BraceError::TooManyExpansions(131072)
530 );
531 assert_eq!(escape("a b"), "\"a b\"");
532 assert_eq!(escape("ab"), "ab");
533 }
534}