use std::fs::File;
use std::sync::{Arc, Mutex};
use super::{ExecResult, Interpreter};
use crate::bun_shell::builtin::{
open_redirect_file, which, Builtin, BuiltinIn, BuiltinKind, BuiltinOut,
};
use crate::bun_shell::builtins;
use crate::bun_shell::env::{path_env, ShellExecEnv};
use crate::bun_shell::expansion::ExpandOpts;
use crate::bun_shell::io::{redirect_flags as rf, Reader, ShellIO, Writer};
use crate::bun_shell::parser::{Atom, Cmd, Redirect, SimpleAtom};
use crate::bun_shell::subprocess::{
is_batch_file, run_subprocess, Dup, InOverride, OutOverride, Overrides, SubprocessOptions,
SubprocessResult,
};
use crate::bun_shell::{OutBuffer, ShellError, ShellValue};
fn is_declaration_utility(atom: Option<&Atom>) -> bool {
matches!(atom, Some(Atom::Simple(SimpleAtom::Text(t))) if t == "export")
}
fn is_assignment_word(atom: &Atom) -> bool {
let Some(SimpleAtom::Text(text)) = atom.atoms().first() else {
return false;
};
let Some(eq) = text.find('=') else {
return false;
};
let name = &text[..eq];
let mut chars = name.chars();
matches!(chars.next(), Some(c) if c.is_ascii_alphabetic() || c == '_')
&& chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
fn has_cmd_exe_special(arg: &str) -> bool {
arg.chars()
.any(|c| matches!(c, '&' | '|' | '<' | '>' | '^' | '%' | '"' | '\r' | '\n'))
}
fn out_buffer(value: &ShellValue) -> Option<OutBuffer> {
match value {
ShellValue::OutBuffer(b) => Some(b.clone()),
ShellValue::Bytes(bytes) => Some(OutBuffer(Arc::new(Mutex::new(bytes.clone())))),
_ => None,
}
}
fn in_bytes(value: &ShellValue) -> Option<Vec<u8>> {
match value {
ShellValue::OutBuffer(b) => Some(b.contents()),
ShellValue::Bytes(bytes) => Some(bytes.clone()),
_ => None,
}
}
fn unknown_js_value(value: &ShellValue) -> ShellError {
ShellError::system(format!("Unknown JS value used in shell: {value:?}"))
}
fn redirect_overrides_for_file(flags: u8, file: File) -> Overrides {
let file = Arc::new(file);
let mut o = Overrides::default();
if flags & rf::STDIN != 0 {
o.stdin = Some(InOverride::File(Arc::clone(&file)));
}
if flags & rf::DUPLICATE_OUT != 0 {
o.stdout = Some(OutOverride::File(Arc::clone(&file)));
o.stderr = Some(OutOverride::File(file));
} else {
if flags & rf::STDOUT != 0 {
o.stdout = Some(OutOverride::File(Arc::clone(&file)));
}
if flags & rf::STDERR != 0 {
o.stderr = Some(OutOverride::File(file));
}
}
o
}
#[derive(Default)]
struct BuiltinRedirect {
stdin: Option<BuiltinIn>,
stdout: Option<BuiltinOut>,
stderr: Option<BuiltinOut>,
dup_to_stdout: Option<bool>,
}
impl Interpreter {
pub(super) async fn cmd(
&self,
node: &Cmd,
shell: &mut ShellExecEnv,
io: &ShellIO,
) -> ExecResult {
if !node.assigns.is_empty() {
let code = self.assigns(&node.assigns, shell, true).await?;
if code != 0 {
return Ok(code);
}
}
let mut redirect_file = None;
if let Some(Redirect::Atom(atom)) = &node.redirect_file {
match self.expand(atom, shell, ExpandOpts::default()).await {
Ok(out) => {
redirect_file = Some(if out.bounds.is_empty() {
out.buf
} else {
String::new()
})
}
Err(e) => {
let msg = format!("{}\n", e.display()?);
return self.cmd_write_failing_error(io, shell, &msg).await;
}
}
}
let mut args: Vec<String> = Vec::new();
let mut exit_code = None;
let declaration = is_declaration_utility(node.name_and_args.first());
for (idx, atom) in node.name_and_args.iter().enumerate() {
let opts = ExpandOpts {
is_assign: false,
assign_ctx: idx > 0 && declaration && is_assignment_word(atom),
};
let out = match self.expand(atom, shell, opts).await {
Ok(out) => out,
Err(e) => {
let msg = format!("{}\n", e.display()?);
return self.cmd_write_failing_error(io, shell, &msg).await;
}
};
if node.name_and_args.len() == 1
&& matches!(atom, Atom::Simple(SimpleAtom::CmdSubst(_)))
{
exit_code = out.out_exit_code;
}
if !out.bounds.is_empty() {
args.extend(out.words());
} else if !out.buf.is_empty() || out.has_quoted_empty {
args.push(out.buf);
}
}
if args.first().is_none_or(String::is_empty) {
return Ok(exit_code.unwrap_or(0));
}
match BuiltinKind::from_argv0(&args[0]).filter(|k| builtins::implemented(*k)) {
Some(kind) => {
self.run_builtin(kind, args, node, redirect_file, shell, io)
.await
}
None => {
self.run_external(args, node, redirect_file, shell, io)
.await
}
}
}
fn jsobj(&self, idx: usize) -> Result<&ShellValue, ShellError> {
self.jsobjs
.get(idx)
.ok_or_else(|| ShellError::system("Invalid JS object reference in shell"))
}
async fn run_builtin(
&self,
kind: BuiltinKind,
mut args: Vec<String>,
node: &Cmd,
redirect_file: Option<String>,
shell: &mut ShellExecEnv,
io: &ShellIO,
) -> ExecResult {
let flags = node.redirect.bits();
let mut r = BuiltinRedirect::default();
match &node.redirect_file {
Some(Redirect::Atom(_)) => {
let file = redirect_file.unwrap_or_default();
if file.is_empty() {
let msg = format!("bun: ambiguous redirect: at `{}`\n", kind.as_str());
return self.cmd_write_failing_error(io, shell, &msg).await;
}
let fd = match open_redirect_file(&shell.cwd, &file, flags) {
Ok(fd) => fd,
Err(e) => {
let msg = format!("bun: {}: {file}", e.message());
return self.cmd_write_failing_error(io, shell, &msg).await;
}
};
if flags & rf::STDIN != 0 {
r.stdin = Some(BuiltinIn::Fd(Reader::file(fd)));
} else if flags & (rf::STDOUT | rf::STDERR) != 0 {
let writer = Writer::file(fd);
let out = || BuiltinOut::Fd {
writer: writer.clone(),
captured: None,
};
if flags & rf::STDOUT != 0 {
r.stdout = Some(out());
}
if flags & rf::STDERR != 0 {
r.stderr = Some(out());
}
}
}
Some(Redirect::JsBuf(idx)) => {
let value = self.jsobj(*idx)?;
let Some(buf) = out_buffer(value) else {
return Err(unknown_js_value(value));
};
if flags & rf::STDIN != 0 {
r.stdin = Some(BuiltinIn::ArrayBuf(buf.contents()));
}
if flags & rf::STDOUT != 0 {
r.stdout = Some(BuiltinOut::array_buf(buf.clone()));
}
if flags & rf::STDERR != 0 {
r.stderr = Some(BuiltinOut::array_buf(buf));
}
}
None if flags & rf::DUPLICATE_OUT != 0 => {
if flags & rf::STDOUT != 0 {
r.dup_to_stdout = Some(true);
}
if flags & rf::STDERR != 0 {
r.dup_to_stdout = Some(false);
}
}
None => {}
}
args.remove(0);
let mut b = Builtin::new(kind, args, shell, io);
if let Some(stdin) = r.stdin {
b.stdin = stdin;
}
if let Some(stdout) = r.stdout {
b.stdout = stdout;
}
if let Some(stderr) = r.stderr {
b.stderr = stderr;
}
match r.dup_to_stdout {
Some(true) => b.stderr = b.stdout.clone(),
Some(false) => b.stdout = b.stderr.clone(),
None => {}
}
Ok(builtins::run(&mut b).await)
}
fn subproc_js_redirect(&self, value: &ShellValue, flags: u8) -> Result<Overrides, ShellError> {
let (Some(bytes), Some(buf)) = (in_bytes(value), out_buffer(value)) else {
return Err(unknown_js_value(value));
};
let dup_out = flags & rf::DUPLICATE_OUT != 0;
let mut o = Overrides::default();
if flags & rf::STDIN != 0 {
o.stdin = Some(InOverride::Bytes(bytes));
}
if dup_out || flags & rf::STDOUT != 0 {
o.stdout = Some(OutOverride::Buffer(buf.clone()));
}
if dup_out || flags & rf::STDERR != 0 {
o.stderr = Some(OutOverride::Buffer(buf));
}
Ok(o)
}
async fn run_external(
&self,
args: Vec<String>,
node: &Cmd,
redirect_file: Option<String>,
shell: &ShellExecEnv,
io: &ShellIO,
) -> ExecResult {
let env = shell.child_env();
let Some(resolved) = which(path_env(&env), &shell.cwd, &args[0]) else {
let msg = format!("bun: command not found: {}\n", args[0]);
return self.cmd_write_failing_error(io, shell, &msg).await;
};
if cfg!(windows) && is_batch_file(&resolved) {
if let Some(unsafe_arg) = args[1..].iter().find(|a| has_cmd_exe_special(a)) {
let msg = format!(
"bun: refusing to pass argument with cmd.exe special characters to a batch file: {unsafe_arg}\n"
);
return self.cmd_write_failing_error(io, shell, &msg).await;
}
}
let mut argv = args;
argv[0] = resolved;
let flags = node.redirect.bits();
let mut overrides = Overrides::default();
let mut dup = None;
match &node.redirect_file {
Some(Redirect::Atom(_)) => {
let file = redirect_file.unwrap_or_default();
if file.is_empty() {
let msg = format!("bun: ambiguous redirect: at `{}`\n", argv[0]);
return self.cmd_write_failing_error(io, shell, &msg).await;
}
match open_redirect_file(&shell.cwd, &file, flags) {
Ok(fd) => overrides = redirect_overrides_for_file(flags, fd),
Err(e) => {
let msg = format!("bun: {}: {file}", e.message());
return self.cmd_write_failing_error(io, shell, &msg).await;
}
}
}
Some(Redirect::JsBuf(idx)) => {
overrides = self.subproc_js_redirect(self.jsobj(*idx)?, flags)?;
}
None if flags & rf::DUPLICATE_OUT != 0 => {
if flags & rf::STDOUT != 0 {
dup = Some(Dup::StderrToStdout);
} else if flags & rf::STDERR != 0 {
dup = Some(Dup::StdoutToStderr);
}
}
None => {}
}
let result = run_subprocess(SubprocessOptions {
args: argv,
cwd: &shell.cwd,
env,
io,
flags,
overrides,
dup,
buffered_stdout: shell.buffered_stdout.clone(),
buffered_stderr: shell.buffered_stderr.clone(),
})
.await;
match result {
SubprocessResult::Exited(code) => Ok(code),
SubprocessResult::SpawnError(e) => {
let msg = format!("{}\n", e.display());
self.cmd_write_failing_error(io, shell, &msg).await
}
}
}
}