command_stream/bun_shell/
mod.rs1pub(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 throws: bool,
264}
265
266impl Default for Shell {
267 fn default() -> Self {
268 Self::new()
269 }
270}
271
272impl Shell {
273 pub fn new() -> Self {
274 Self {
275 cwd: None,
276 env: None,
277 throws: true,
278 }
279 }
280
281 pub fn cwd(&mut self, cwd: Option<impl Into<PathBuf>>) -> &mut Self {
283 self.cwd = cwd.map(Into::into);
284 self
285 }
286
287 pub fn env(&mut self, env: Option<HashMap<String, String>>) -> &mut Self {
289 self.env = env;
290 self
291 }
292
293 pub fn nothrow(&mut self) -> &mut Self {
294 self.throws = false;
295 self
296 }
297
298 pub fn throws(&mut self, throws: bool) -> &mut Self {
299 self.throws = throws;
300 self
301 }
302
303 pub fn command(
306 &self,
307 strings: &[&str],
308 values: Vec<ShellValue>,
309 ) -> Result<ShellCommand, ShellError> {
310 let mut cmd = ShellCommand::parse(strings, values)?;
311 cmd.throws = self.throws;
312 if let Some(cwd) = &self.cwd {
313 cmd = cmd.cwd(cwd.clone());
314 }
315 if let Some(env) = &self.env {
316 cmd = cmd.env(env.clone());
317 }
318 Ok(cmd)
319 }
320}
321
322pub fn shell(strings: &[&str], values: Vec<ShellValue>) -> Result<ShellCommand, ShellError> {
324 Shell::new().command(strings, values)
325}
326
327pub struct ShellCommand {
330 script: ParsedScript,
331 cwd: Option<PathBuf>,
332 env: HashMap<String, String>,
333 quiet: bool,
334 throws: bool,
335}
336
337impl fmt::Debug for ShellCommand {
340 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
341 let mut env: Vec<&str> = self.env.keys().map(String::as_str).collect();
342 env.sort_unstable();
343 f.debug_struct("ShellCommand")
344 .field("script", &self.script)
345 .field("cwd", &self.cwd)
346 .field("env", &env)
347 .field("quiet", &self.quiet)
348 .field("throws", &self.throws)
349 .finish()
350 }
351}
352
353#[derive(Debug)]
355pub(crate) struct ParsedScript {
356 pub(crate) ast: parser::Script,
357 #[cfg_attr(not(test), allow(dead_code))]
360 pub(crate) jsstrings: Vec<String>,
361 pub(crate) jsobjs: Vec<ShellValue>,
363}
364
365impl ShellCommand {
366 fn parse(strings: &[&str], values: Vec<ShellValue>) -> Result<Self, ShellError> {
367 let src = template::build_shell_source(strings, values).map_err(ShellError::parse)?;
368 let ast = parser::parse(&src.script, &src.jsstrings, src.jsobjs.len())
369 .map_err(ShellError::parse)?;
370 Ok(Self {
371 script: ParsedScript {
372 ast,
373 jsstrings: src.jsstrings,
374 jsobjs: src.jsobjs,
375 },
376 cwd: None,
377 env: std::env::vars().collect(),
378 quiet: false,
379 throws: true,
380 })
381 }
382
383 pub fn cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
385 let cwd = cwd.into();
386 let s = cwd.to_string_lossy();
387 self.cwd = if s.is_empty() || s == "." || s == "./" {
388 std::env::current_dir().ok()
389 } else {
390 Some(cwd)
391 };
392 self
393 }
394
395 pub fn env(mut self, env: HashMap<String, String>) -> Self {
397 self.env = env;
398 self
399 }
400
401 pub fn quiet(mut self) -> Self {
403 self.quiet = true;
404 self
405 }
406
407 pub fn nothrow(mut self) -> Self {
409 self.throws = false;
410 self
411 }
412
413 pub fn throws(mut self, throws: bool) -> Self {
414 self.throws = throws;
415 self
416 }
417
418 pub async fn run(self) -> Result<ShellOutput, ShellError> {
420 let mut env: Vec<(String, String)> = self.env.into_iter().collect();
422 env.sort_unstable_by(|a, b| a.0.cmp(&b.0));
423 let (interp, mut root) = interpreter::Interpreter::new(interpreter::InterpreterOptions {
424 jsobjs: self.script.jsobjs,
425 env: env.into_iter().collect(),
426 cwd: self.cwd.map(|p| p.to_string_lossy().into_owned()),
427 quiet: self.quiet,
428 argv: std::env::args().collect(),
429 })?;
430 let out = interp.run(&self.script.ast, &mut root).await?;
431 let output = ShellOutput {
432 stdout: out.stdout,
433 stderr: out.stderr,
434 exit_code: out.exit_code,
435 };
436 if self.throws && output.exit_code != 0 {
437 return Err(ShellError::exit(output));
438 }
439 Ok(output)
440 }
441
442 pub async fn text(self) -> Result<String, ShellError> {
444 Ok(self.quiet().run().await?.text())
445 }
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451
452 fn parse_error(strings: &[&str], values: Vec<ShellValue>) -> ShellError {
453 match shell(strings, values) {
454 Ok(_) => panic!("expected a parse error for {strings:?}"),
455 Err(e) => e,
456 }
457 }
458
459 #[test]
460 fn frontend_errors_are_parse_errors_with_the_js_message() {
461 let e = parse_error(&["echo ("], vec![]);
462 assert_eq!(e.kind, ShellErrorKind::Parse);
463 assert_eq!(e.message, "Unclosed subshell");
464 let e = parse_error(&["echo $(echo"], vec![]);
465 assert_eq!(e.message, "Unclosed command substitution");
466 let e = parse_error(&["echo hi &"], vec![]);
467 assert_eq!(
468 e.message,
469 "Background commands \"&\" are not supported yet."
470 );
471 let e = parse_error(&["echo ", ""], vec![ShellValue::Str("a\0b".into())]);
472 assert_eq!(e.kind, ShellErrorKind::Parse);
473 }
474
475 #[test]
476 fn debug_shows_env_names_but_not_values() {
477 let env = HashMap::from([("TOKEN".to_string(), "s3cret".to_string())]);
478 let cmd = shell(&["echo hi"], vec![]).unwrap().env(env);
479 let debug = format!("{cmd:?}");
480 assert!(debug.contains("TOKEN"), "{debug}");
481 assert!(!debug.contains("s3cret"), "{debug}");
482 }
483
484 #[test]
485 fn parse_keeps_interpolated_strings_and_objects() {
486 let cmd = shell(
487 &["cat ", " < ", ""],
488 vec![ShellValue::from("a b"), ShellValue::Bytes(b"x".to_vec())],
489 )
490 .unwrap();
491 assert_eq!(cmd.script.jsstrings, ["a b"]);
492 assert_eq!(cmd.script.jsobjs.len(), 1);
493 assert_eq!(cmd.script.ast.stmts.len(), 1);
494 }
495
496 #[test]
497 fn braces_and_escape() {
498 assert_eq!(braces("x{a,b}").unwrap(), ["xa", "xb"]);
499 assert_eq!(
500 braces(&"{a,b}".repeat(17)).unwrap_err(),
501 BraceError::TooManyExpansions(131072)
502 );
503 assert_eq!(escape("a b"), "\"a b\"");
504 assert_eq!(escape("ab"), "ab");
505 }
506}