use crate::commands::{CommandContext, CommandResult};
use crate::error::RustBashError;
use crate::interpreter::builtins::{self, resolve_path};
use crate::interpreter::expansion::{expand_word_mut, expand_word_to_string_mut};
use crate::interpreter::{
CallFrame, ExecResult, ExecutionCounters, FunctionDef, InterpreterState, PersistentFd,
Variable, VariableAttrs, VariableValue, execute_trap, parse, set_array_element, set_variable,
};
use brush_parser::ast;
use brush_parser::ast::SourceLocation;
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
fn expand_ps4(state: &mut InterpreterState) -> String {
let raw = state
.env
.get("PS4")
.map(|v| v.value.as_scalar().to_string());
match raw {
Some(s) if !s.is_empty() => {
let word = brush_parser::ast::Word {
value: s,
loc: Default::default(),
};
expand_word_to_string_mut(&word, state).unwrap_or_else(|_| "+ ".to_string())
}
Some(_) => "+ ".to_string(), None => String::new(), }
}
fn xtrace_quote(word: &str) -> String {
if word.is_empty() {
return "''".to_string();
}
let has_single_quote = word.contains('\'');
let needs_quoting = word
.chars()
.any(|c| c.is_whitespace() || c == '\'' || c == '"' || c == '\\' || (c as u32) < 0x20);
if !needs_quoting {
return word.to_string();
}
if has_single_quote {
let mut out = String::new();
let mut in_squote = false;
for c in word.chars() {
if c == '\'' {
if in_squote {
out.push('\''); in_squote = false;
}
out.push_str("\\'");
} else {
if !in_squote {
out.push('\''); in_squote = true;
}
out.push(c);
}
}
if in_squote {
out.push('\'');
}
out
} else {
format!("'{word}'")
}
}
fn format_xtrace_command(ps4: &str, cmd: &str, args: &[String]) -> String {
let mut parts = Vec::with_capacity(1 + args.len());
parts.push(xtrace_quote(cmd));
for a in args {
parts.push(xtrace_quote(a));
}
format!("{ps4}{}\n", parts.join(" "))
}
fn check_errexit(state: &mut InterpreterState) {
if state.shell_opts.errexit
&& state.last_exit_code != 0
&& state.errexit_suppressed == 0
&& !state.in_trap
{
state.should_exit = true;
}
}
fn check_limits(state: &InterpreterState) -> Result<(), RustBashError> {
if state.counters.command_count > state.limits.max_command_count {
return Err(RustBashError::LimitExceeded {
limit_name: "max_command_count",
limit_value: state.limits.max_command_count,
actual_value: state.counters.command_count,
});
}
if state.counters.output_size > state.limits.max_output_size {
return Err(RustBashError::LimitExceeded {
limit_name: "max_output_size",
limit_value: state.limits.max_output_size,
actual_value: state.counters.output_size,
});
}
if state.counters.start_time.elapsed() > state.limits.max_execution_time {
return Err(RustBashError::Timeout);
}
Ok(())
}
pub fn execute_program(
program: &ast::Program,
state: &mut InterpreterState,
) -> Result<ExecResult, RustBashError> {
let mut result = ExecResult::default();
for complete_command in &program.complete_commands {
if state.should_exit {
break;
}
let r = execute_compound_list(complete_command, state, "")?;
state.counters.output_size += r.stdout.len() + r.stderr.len();
check_limits(state)?;
result.stdout.push_str(&r.stdout);
result.stderr.push_str(&r.stderr);
result.exit_code = r.exit_code;
state.last_exit_code = r.exit_code;
}
Ok(result)
}
fn execute_compound_list(
list: &ast::CompoundList,
state: &mut InterpreterState,
stdin: &str,
) -> Result<ExecResult, RustBashError> {
let mut result = ExecResult::default();
for item in &list.0 {
if state.should_exit || state.control_flow.is_some() {
break;
}
let ast::CompoundListItem(and_or_list, _separator) = item;
let r = match execute_and_or_list(and_or_list, state, stdin) {
Ok(r) => r,
Err(RustBashError::Execution(msg)) if msg.contains("unbound variable") => {
state.should_exit = true;
state.last_exit_code = 1;
ExecResult {
stderr: format!("rust-bash: {msg}\n"),
exit_code: 1,
..Default::default()
}
}
Err(e) => return Err(e),
};
result.stdout.push_str(&r.stdout);
result.stderr.push_str(&r.stderr);
result.exit_code = r.exit_code;
state.last_exit_code = r.exit_code;
if r.exit_code != 0
&& !state.in_trap
&& state.errexit_suppressed == 0
&& let Some(err_cmd) = state.traps.get("ERR").cloned()
&& !err_cmd.is_empty()
{
let trap_r = execute_trap(&err_cmd, state)?;
result.stdout.push_str(&trap_r.stdout);
result.stderr.push_str(&trap_r.stderr);
}
}
Ok(result)
}
fn execute_and_or_list(
aol: &ast::AndOrList,
state: &mut InterpreterState,
stdin: &str,
) -> Result<ExecResult, RustBashError> {
let has_chain = !aol.additional.is_empty();
if has_chain {
state.errexit_suppressed += 1;
}
let mut result = execute_pipeline(&aol.first, state, stdin)?;
if has_chain {
state.errexit_suppressed -= 1;
}
state.last_exit_code = result.exit_code;
if !has_chain {
check_errexit(state);
if state.should_exit {
return Ok(result);
}
}
for (idx, and_or) in aol.additional.iter().enumerate() {
if state.should_exit || state.control_flow.is_some() {
break;
}
let (should_run, pipeline) = match and_or {
ast::AndOr::And(p) => (result.exit_code == 0, p),
ast::AndOr::Or(p) => (result.exit_code != 0, p),
};
if should_run {
let is_last = idx == aol.additional.len() - 1;
if !is_last {
state.errexit_suppressed += 1;
}
let r = execute_pipeline(pipeline, state, stdin)?;
if !is_last {
state.errexit_suppressed -= 1;
}
result.stdout.push_str(&r.stdout);
result.stderr.push_str(&r.stderr);
result.exit_code = r.exit_code;
state.last_exit_code = r.exit_code;
if is_last {
check_errexit(state);
}
}
}
Ok(result)
}
fn execute_pipeline(
pipeline: &ast::Pipeline,
state: &mut InterpreterState,
stdin: &str,
) -> Result<ExecResult, RustBashError> {
let timed = pipeline.timed.is_some();
let start = if timed {
Some(crate::platform::Instant::now())
} else {
None
};
let mut pipe_data = stdin.to_string();
let mut pipe_data_bytes: Option<Vec<u8>> = None;
let mut combined_stderr = String::new();
let mut exit_code = 0;
let mut exit_codes: Vec<i32> = Vec::new();
let is_actual_pipe = pipeline.seq.len() > 1;
let saved_stdin_offset = state.stdin_offset;
if pipeline.bang {
state.errexit_suppressed += 1;
}
for (idx, command) in pipeline.seq.iter().enumerate() {
if state.should_exit || state.control_flow.is_some() {
break;
}
if idx > 0 {
state.stdin_offset = 0;
}
state.pipe_stdin_bytes = pipe_data_bytes.take();
let r = execute_command(command, state, &pipe_data)?;
if let Some(bytes) = r.stdout_bytes {
pipe_data_bytes = Some(bytes);
pipe_data = String::new();
} else {
pipe_data = r.stdout;
pipe_data_bytes = None;
}
combined_stderr.push_str(&r.stderr);
exit_code = r.exit_code;
exit_codes.push(r.exit_code);
}
state.pipe_stdin_bytes = None;
if is_actual_pipe {
state.stdin_offset = saved_stdin_offset;
}
if pipeline.bang {
state.errexit_suppressed -= 1;
}
if state.shell_opts.pipefail {
exit_code = exit_codes
.iter()
.rev()
.copied()
.find(|&c| c != 0)
.unwrap_or(0);
}
let exit_code = if pipeline.bang {
i32::from(exit_code == 0)
} else {
exit_code
};
let mut pipestatus_map = std::collections::BTreeMap::new();
for (i, code) in exit_codes.iter().enumerate() {
pipestatus_map.insert(i, code.to_string());
}
state.env.insert(
"PIPESTATUS".to_string(),
Variable {
value: VariableValue::IndexedArray(pipestatus_map),
attrs: VariableAttrs::empty(),
},
);
if let Some(start) = start {
let elapsed = start.elapsed();
let total_secs = elapsed.as_secs_f64();
let mins = total_secs as u64 / 60;
let secs = total_secs - (mins as f64 * 60.0);
combined_stderr.push_str(&format!(
"\nreal\t{}m{:.3}s\nuser\t0m0.000s\nsys\t0m0.000s\n",
mins, secs
));
}
let final_stdout = if let Some(bytes) = pipe_data_bytes {
String::from_utf8_lossy(&bytes).into_owned()
} else {
pipe_data
};
Ok(ExecResult {
stdout: final_stdout,
stderr: combined_stderr,
exit_code,
stdout_bytes: None,
})
}
fn execute_command(
command: &ast::Command,
state: &mut InterpreterState,
stdin: &str,
) -> Result<ExecResult, RustBashError> {
if let Some(loc) = command.location() {
state.current_lineno = loc.start.line;
}
if state.shell_opts.noexec && !matches!(command, ast::Command::Simple(_)) {
return Ok(ExecResult::default());
}
let result = match command {
ast::Command::Simple(simple_cmd) => execute_simple_command(simple_cmd, state, stdin),
ast::Command::Compound(compound, redirects) => {
execute_compound_command(compound, redirects.as_ref(), state, stdin)
}
ast::Command::Function(func_def) => {
match expand_word_to_string_mut(&func_def.fname, state) {
Ok(name) => {
state.functions.insert(
name,
FunctionDef {
body: func_def.body.clone(),
},
);
Ok(ExecResult::default())
}
Err(e) => Err(e),
}
}
ast::Command::ExtendedTest(ext_test) => execute_extended_test(&ext_test.expr, state),
};
match result {
Err(RustBashError::ExpansionError {
message,
exit_code,
should_exit,
}) => {
state.last_exit_code = exit_code;
if should_exit {
state.should_exit = true;
}
Ok(ExecResult {
stderr: format!("rust-bash: {message}\n"),
exit_code,
..Default::default()
})
}
Err(RustBashError::FailGlob { pattern }) => {
state.last_exit_code = 1;
Ok(ExecResult {
stderr: format!("rust-bash: no match: {pattern}\n"),
exit_code: 1,
..Default::default()
})
}
other => other,
}
}
#[derive(Debug, Clone)]
enum Assignment {
Scalar { name: String, value: String },
IndexedArray {
name: String,
elements: Vec<(Option<usize>, String)>,
},
AssocArray {
name: String,
elements: Vec<(String, String)>,
},
ArrayElement {
name: String,
index: String,
value: String,
},
AppendArrayElement {
name: String,
index: String,
value: String,
},
AppendArray {
name: String,
elements: Vec<(Option<usize>, String)>,
},
AppendAssocArray {
name: String,
elements: Vec<(String, String)>,
},
AppendScalar { name: String, value: String },
}
impl Assignment {
fn name(&self) -> &str {
match self {
Assignment::Scalar { name, .. }
| Assignment::IndexedArray { name, .. }
| Assignment::AssocArray { name, .. }
| Assignment::ArrayElement { name, .. }
| Assignment::AppendArrayElement { name, .. }
| Assignment::AppendArray { name, .. }
| Assignment::AppendAssocArray { name, .. }
| Assignment::AppendScalar { name, .. } => name,
}
}
}
fn process_assignment(
assignment: &ast::Assignment,
append: bool,
state: &mut InterpreterState,
) -> Result<Assignment, RustBashError> {
match (&assignment.name, &assignment.value) {
(ast::AssignmentName::VariableName(name), ast::AssignmentValue::Scalar(w)) => {
let value = expand_word_to_string_mut(w, state)?;
if append {
Ok(Assignment::AppendScalar {
name: name.clone(),
value,
})
} else {
Ok(Assignment::Scalar {
name: name.clone(),
value,
})
}
}
(ast::AssignmentName::VariableName(name), ast::AssignmentValue::Array(items)) => {
let is_assoc = state
.env
.get(name)
.is_some_and(|v| matches!(v.value, VariableValue::AssociativeArray(_)));
if is_assoc {
let mut elements = Vec::new();
for (opt_idx_word, val_word) in items {
let key = if let Some(idx_word) = opt_idx_word {
expand_word_to_string_mut(idx_word, state)?
} else {
String::new()
};
let val = expand_word_to_string_mut(val_word, state)?;
elements.push((key, val));
}
if append {
Ok(Assignment::AppendAssocArray {
name: name.clone(),
elements,
})
} else {
Ok(Assignment::AssocArray {
name: name.clone(),
elements,
})
}
} else {
let mut elements = Vec::new();
for (opt_idx_word, val_word) in items {
let idx = if let Some(idx_word) = opt_idx_word {
let idx_str = expand_word_to_string_mut(idx_word, state)?;
let idx_val =
crate::interpreter::arithmetic::eval_arithmetic(&idx_str, state)?;
if idx_val < 0 {
return Err(RustBashError::Execution(format!(
"negative array subscript: {idx_val}"
)));
}
Some(idx_val as usize)
} else {
None
};
let vals = expand_word_mut(val_word, state)?;
if vals.is_empty() {
elements.push((idx, String::new()));
} else {
for (i, val) in vals.into_iter().enumerate() {
if i == 0 {
elements.push((idx, val));
} else {
elements.push((None, val));
}
}
}
}
if append {
Ok(Assignment::AppendArray {
name: name.clone(),
elements,
})
} else {
Ok(Assignment::IndexedArray {
name: name.clone(),
elements,
})
}
}
}
(
ast::AssignmentName::ArrayElementName(name, index_str),
ast::AssignmentValue::Scalar(w),
) => {
let value = expand_word_to_string_mut(w, state)?;
let index_word = ast::Word {
value: index_str.clone(),
loc: None,
};
let expanded_index = expand_word_to_string_mut(&index_word, state)?;
if append {
Ok(Assignment::AppendArrayElement {
name: name.clone(),
index: expanded_index,
value,
})
} else {
Ok(Assignment::ArrayElement {
name: name.clone(),
index: expanded_index,
value,
})
}
}
(ast::AssignmentName::ArrayElementName(name, _), ast::AssignmentValue::Array(_)) => Err(
RustBashError::Execution(format!("{name}: cannot assign array to array element")),
),
}
}
fn apply_assignment(
assignment: Assignment,
state: &mut InterpreterState,
) -> Result<(), RustBashError> {
match assignment {
Assignment::Scalar { name, value } => {
set_variable(state, &name, value)?;
}
Assignment::IndexedArray { name, elements } => {
if let Some(var) = state.env.get(&name)
&& var.readonly()
{
return Err(RustBashError::Execution(format!(
"{name}: readonly variable"
)));
}
let limit = state.limits.max_array_elements;
let mut map = std::collections::BTreeMap::new();
let mut auto_idx: usize = 0;
for (opt_idx, val) in elements {
let idx = opt_idx.unwrap_or(auto_idx);
if map.len() >= limit {
return Err(RustBashError::LimitExceeded {
limit_name: "max_array_elements",
limit_value: limit,
actual_value: map.len() + 1,
});
}
map.insert(idx, val);
auto_idx = idx + 1;
}
let attrs = state
.env
.get(&name)
.map(|v| v.attrs)
.unwrap_or(VariableAttrs::empty());
state.env.insert(
name,
Variable {
value: VariableValue::IndexedArray(map),
attrs,
},
);
}
Assignment::AssocArray { name, elements } => {
if let Some(var) = state.env.get(&name)
&& var.readonly()
{
return Err(RustBashError::Execution(format!(
"{name}: readonly variable"
)));
}
let limit = state.limits.max_array_elements;
let mut map = std::collections::BTreeMap::new();
for (key, val) in elements {
if map.len() >= limit {
return Err(RustBashError::LimitExceeded {
limit_name: "max_array_elements",
limit_value: limit,
actual_value: map.len() + 1,
});
}
map.insert(key, val);
}
let attrs = state
.env
.get(&name)
.map(|v| v.attrs)
.unwrap_or(VariableAttrs::empty());
state.env.insert(
name,
Variable {
value: VariableValue::AssociativeArray(map),
attrs,
},
);
}
Assignment::ArrayElement { name, index, value } => {
let is_assoc = state
.env
.get(&name)
.is_some_and(|v| matches!(v.value, VariableValue::AssociativeArray(_)));
if is_assoc {
crate::interpreter::set_assoc_element(state, &name, index, value)?;
} else {
let idx = crate::interpreter::arithmetic::eval_arithmetic(&index, state)?;
let uidx = resolve_negative_array_index(idx, &name, state)?;
set_array_element(state, &name, uidx, value)?;
}
}
Assignment::AppendArrayElement { name, index, value } => {
let is_assoc = state
.env
.get(&name)
.is_some_and(|v| matches!(v.value, VariableValue::AssociativeArray(_)));
if is_assoc {
let current = state
.env
.get(&name)
.and_then(|v| match &v.value {
VariableValue::AssociativeArray(map) => map.get(&index).cloned(),
_ => None,
})
.unwrap_or_default();
let new_val = format!("{current}{value}");
crate::interpreter::set_assoc_element(state, &name, index, new_val)?;
} else {
let idx = crate::interpreter::arithmetic::eval_arithmetic(&index, state)?;
let uidx = resolve_negative_array_index(idx, &name, state)?;
let current = state
.env
.get(&name)
.and_then(|v| match &v.value {
VariableValue::IndexedArray(map) => map.get(&uidx).cloned(),
VariableValue::Scalar(s) if uidx == 0 => Some(s.clone()),
_ => None,
})
.unwrap_or_default();
let new_val = format!("{current}{value}");
set_array_element(state, &name, uidx, new_val)?;
}
}
Assignment::AppendArray { name, elements } => {
let start_idx = match state.env.get(&name) {
Some(var) => match &var.value {
VariableValue::IndexedArray(map) => {
map.keys().next_back().map(|k| k + 1).unwrap_or(0)
}
VariableValue::Scalar(s) if s.is_empty() => 0,
VariableValue::Scalar(_) => 1,
VariableValue::AssociativeArray(_) => 0,
},
None => 0,
};
if !state.env.contains_key(&name) {
state.env.insert(
name.clone(),
Variable {
value: VariableValue::IndexedArray(std::collections::BTreeMap::new()),
attrs: VariableAttrs::empty(),
},
);
}
if let Some(var) = state.env.get_mut(&name)
&& let VariableValue::Scalar(s) = &var.value
{
let mut map = std::collections::BTreeMap::new();
if !s.is_empty() {
map.insert(0, s.clone());
}
var.value = VariableValue::IndexedArray(map);
}
let mut auto_idx = start_idx;
for (opt_idx, val) in elements {
let idx = opt_idx.unwrap_or(auto_idx);
set_array_element(state, &name, idx, val)?;
auto_idx = idx + 1;
}
}
Assignment::AppendAssocArray { name, elements } => {
if !state.env.contains_key(&name) {
state.env.insert(
name.clone(),
Variable {
value: VariableValue::AssociativeArray(std::collections::BTreeMap::new()),
attrs: VariableAttrs::empty(),
},
);
}
for (key, val) in elements {
crate::interpreter::set_assoc_element(state, &name, key, val)?;
}
}
Assignment::AppendScalar { name, value } => {
let target = crate::interpreter::resolve_nameref(&name, state)?;
let is_integer = state
.env
.get(&target)
.is_some_and(|v| v.attrs.contains(VariableAttrs::INTEGER));
if is_integer {
let current = state
.env
.get(&target)
.map(|v| v.value.as_scalar().to_string())
.unwrap_or_else(|| "0".to_string());
let expr = format!("{current}+{value}");
set_variable(state, &name, expr)?;
} else {
match state.env.get(&target) {
Some(var) => {
let new_val = format!("{}{}", var.value.as_scalar(), value);
set_variable(state, &name, new_val)?;
}
None => {
set_variable(state, &name, value)?;
}
}
}
}
}
Ok(())
}
fn resolve_negative_array_index(
idx: i64,
name: &str,
state: &InterpreterState,
) -> Result<usize, RustBashError> {
if idx >= 0 {
return Ok(idx as usize);
}
let max_key = state.env.get(name).and_then(|v| match &v.value {
VariableValue::IndexedArray(map) => map.keys().next_back().copied(),
VariableValue::Scalar(_) => Some(0),
_ => None,
});
match max_key {
Some(mk) => {
let resolved = mk as i64 + 1 + idx;
if resolved < 0 {
Err(RustBashError::Execution(format!(
"{name}: bad array subscript"
)))
} else {
Ok(resolved as usize)
}
}
None => Err(RustBashError::Execution(format!(
"{name}: bad array subscript"
))),
}
}
fn apply_assignment_shell_error(
assignment: Assignment,
state: &mut InterpreterState,
result: &mut ExecResult,
) -> Result<(), RustBashError> {
match apply_assignment(assignment, state) {
Ok(()) => Ok(()),
Err(RustBashError::Execution(msg)) => {
result.stderr.push_str(&format!("rust-bash: {msg}\n"));
result.exit_code = 1;
state.last_exit_code = 1;
Ok(())
}
Err(other) => Err(other),
}
}
fn execute_simple_command(
cmd: &ast::SimpleCommand,
state: &mut InterpreterState,
stdin: &str,
) -> Result<ExecResult, RustBashError> {
if state.shell_opts.noexec {
return Ok(ExecResult::default());
}
let mut assignments: Vec<Assignment> = Vec::new();
let mut redirects: Vec<&ast::IoRedirect> = Vec::new();
let mut proc_sub_temps: Vec<String> = Vec::new();
let mut deferred_write_subs: Vec<(&ast::CompoundList, String)> = Vec::new();
if let Some(prefix) = &cmd.prefix {
for item in &prefix.0 {
match item {
ast::CommandPrefixOrSuffixItem::AssignmentWord(assignment, _word) => {
let a = process_assignment(assignment, assignment.append, state)?;
assignments.push(a);
}
ast::CommandPrefixOrSuffixItem::IoRedirect(redir) => {
redirects.push(redir);
}
ast::CommandPrefixOrSuffixItem::ProcessSubstitution(kind, subshell) => {
let path = expand_process_substitution(
kind,
&subshell.list,
state,
&mut deferred_write_subs,
)?;
proc_sub_temps.push(path);
}
_ => {}
}
}
}
let cmd_name = cmd
.word_or_name
.as_ref()
.map(|w| expand_word_to_string_mut(w, state))
.transpose()?;
let mut args: Vec<String> = Vec::new();
if let Some(suffix) = &cmd.suffix {
for item in &suffix.0 {
match item {
ast::CommandPrefixOrSuffixItem::Word(w) => match expand_word_mut(w, state) {
Ok(expanded) => args.extend(expanded),
Err(RustBashError::FailGlob { pattern }) => {
state.last_exit_code = 1;
return Ok(ExecResult {
stderr: format!("rust-bash: no match: {pattern}\n"),
exit_code: 1,
..Default::default()
});
}
Err(e) => return Err(e),
},
ast::CommandPrefixOrSuffixItem::IoRedirect(redir) => {
redirects.push(redir);
}
ast::CommandPrefixOrSuffixItem::AssignmentWord(assignment, _word) => {
let name = match &assignment.name {
ast::AssignmentName::VariableName(n) => n.clone(),
ast::AssignmentName::ArrayElementName(n, idx) => {
let idx_word = ast::Word {
value: idx.clone(),
loc: None,
};
let expanded_idx = expand_word_to_string_mut(&idx_word, state)?;
format!("{n}[{expanded_idx}]")
}
};
match &assignment.value {
ast::AssignmentValue::Scalar(w) => {
let value = expand_word_to_string_mut(w, state)?;
args.push(format!("{name}={value}"));
}
ast::AssignmentValue::Array(items) => {
let mut parts = Vec::new();
for (opt_idx_word, val_word) in items {
let vals = expand_word_mut(val_word, state)?;
if let Some(idx_word) = opt_idx_word {
let idx_str = expand_word_to_string_mut(idx_word, state)?;
let first = vals.first().cloned().unwrap_or_default();
parts.push(format!("[{idx_str}]={first}"));
for v in vals.into_iter().skip(1) {
parts.push(v);
}
} else {
parts.extend(vals);
}
}
args.push(format!("{name}=({})", parts.join(" ")));
}
}
}
ast::CommandPrefixOrSuffixItem::ProcessSubstitution(kind, subshell) => {
let path = expand_process_substitution(
kind,
&subshell.list,
state,
&mut deferred_write_subs,
)?;
proc_sub_temps.push(path.clone());
args.push(path);
}
}
}
}
let Some(cmd_name) = cmd_name else {
if state.shell_opts.xtrace && !assignments.is_empty() {
let ps4 = expand_ps4(state);
let mut trace = String::new();
for a in &assignments {
let part = match a {
Assignment::Scalar { name, value } => format!("{name}={value}"),
Assignment::IndexedArray { name, elements, .. } => {
let vals: Vec<String> =
elements.iter().map(|(_, v)| xtrace_quote(v)).collect();
format!("{name}=({})", vals.join(" "))
}
Assignment::ArrayElement {
name, index, value, ..
} => format!("{name}[{index}]={value}"),
Assignment::AppendArrayElement {
name, index, value, ..
} => format!("{name}[{index}]+={value}"),
Assignment::AppendArray { name, elements, .. } => {
let vals: Vec<String> =
elements.iter().map(|(_, v)| xtrace_quote(v)).collect();
format!("{name}+=({})", vals.join(" "))
}
Assignment::AssocArray { name, .. } => format!("{name}=(...)"),
Assignment::AppendAssocArray { name, .. } => format!("{name}+=(...)"),
Assignment::AppendScalar { name, value } => format!("{name}+={value}"),
};
trace.push_str(&format!("{ps4}{part}\n"));
}
let mut result = ExecResult {
stderr: trace,
..ExecResult::default()
};
for a in assignments {
apply_assignment_shell_error(a, state, &mut result)?;
}
if !state.pending_cmdsub_stderr.is_empty() {
let cmdsub_stderr = std::mem::take(&mut state.pending_cmdsub_stderr);
result.stderr = format!("{cmdsub_stderr}{}", result.stderr);
}
return Ok(result);
}
let mut result = ExecResult::default();
for a in assignments {
apply_assignment_shell_error(a, state, &mut result)?;
}
if !state.pending_cmdsub_stderr.is_empty() {
let cmdsub_stderr = std::mem::take(&mut state.pending_cmdsub_stderr);
result.stderr = format!("{cmdsub_stderr}{}", result.stderr);
}
return Ok(result);
};
if cmd_name.is_empty() && args.is_empty() {
let mut result = ExecResult {
exit_code: state.last_exit_code,
..ExecResult::default()
};
for a in assignments {
apply_assignment_shell_error(a, state, &mut result)?;
}
if !state.pending_cmdsub_stderr.is_empty() {
let cmdsub_stderr = std::mem::take(&mut state.pending_cmdsub_stderr);
result.stderr = format!("{cmdsub_stderr}{}", result.stderr);
}
return Ok(result);
}
let (cmd_name, args) = if state.shopt_opts.expand_aliases {
if let Some(expansion) = state.aliases.get(&cmd_name).cloned() {
let mut parts: Vec<String> = expansion
.split_whitespace()
.map(|s| s.to_string())
.collect();
if parts.is_empty() {
(cmd_name, args)
} else {
let new_cmd = parts.remove(0);
parts.extend(args);
(new_cmd, parts)
}
} else {
(cmd_name, args)
}
} else {
(cmd_name, args)
};
if cmd_name == "exec" {
if args.first().map(|a| a.as_str()) == Some("--help")
&& let Some(meta) = builtins::builtin_meta("exec")
&& meta.supports_help_flag
{
return Ok(ExecResult {
stdout: crate::commands::format_help(meta),
stderr: String::new(),
exit_code: 0,
stdout_bytes: None,
});
}
for a in &assignments {
let mut dummy = ExecResult::default();
apply_assignment_shell_error(a.clone(), state, &mut dummy)?;
if dummy.exit_code != 0 {
return Ok(dummy);
}
}
return execute_exec_builtin(&args, &redirects, state, stdin);
}
let mut saved: Vec<(String, Option<Variable>)> = Vec::new();
let mut prefix_stderr = String::new();
for a in &assignments {
saved.push((a.name().to_string(), state.env.get(a.name()).cloned()));
let mut dummy = ExecResult::default();
apply_assignment_shell_error(a.clone(), state, &mut dummy)?;
if dummy.exit_code != 0 {
prefix_stderr.push_str(&dummy.stderr);
}
if let Some(var) = state.env.get_mut(a.name()) {
var.attrs.insert(VariableAttrs::EXPORTED);
}
}
struct RedirProcSub<'a> {
temp_path: String,
kind: &'a ast::ProcessSubstitutionKind,
list: &'a ast::CompoundList,
}
let last_arg = args.last().cloned().unwrap_or_else(|| cmd_name.clone());
let should_trace = state.shell_opts.xtrace;
let pre_ps4 = if should_trace {
Some(expand_ps4(state))
} else {
None
};
let mut inner_result = (|| -> Result<ExecResult, RustBashError> {
let mut redir_proc_subs: Vec<RedirProcSub<'_>> = Vec::new();
for redir in &redirects {
if let ast::IoRedirect::File(
_,
_,
target @ ast::IoFileRedirectTarget::ProcessSubstitution(kind, subshell),
) = redir
{
let temp_path = match kind {
ast::ProcessSubstitutionKind::Read => {
execute_read_process_substitution(&subshell.list, state)?
}
ast::ProcessSubstitutionKind::Write => allocate_proc_sub_temp_file(state, b"")?,
};
proc_sub_temps.push(temp_path.clone());
let key = std::ptr::from_ref(target) as usize;
state.proc_sub_prealloc.insert(key, temp_path.clone());
redir_proc_subs.push(RedirProcSub {
temp_path,
kind,
list: &subshell.list,
});
}
}
let effective_stdin = match get_stdin_from_redirects(&redirects, state, stdin) {
Ok(s) => s,
Err(RustBashError::RedirectFailed(msg)) => {
let mut result = ExecResult {
stderr: format!("rust-bash: {msg}\n"),
exit_code: 1,
..ExecResult::default()
};
state.last_exit_code = 1;
apply_output_redirects(&redirects, &mut result, state)?;
return Ok(result);
}
Err(e) => return Err(e),
};
state.last_argument = last_arg.clone();
let mut result = dispatch_command(&cmd_name, &args, state, &effective_stdin)?;
if !state.pending_cmdsub_stderr.is_empty() {
let cmdsub_stderr = std::mem::take(&mut state.pending_cmdsub_stderr);
result.stderr = format!("{cmdsub_stderr}{}", result.stderr);
}
if let Some(ref ps4) = pre_ps4 {
let mut trace = format_xtrace_command(ps4, &cmd_name, &args);
if matches!(
cmd_name.as_str(),
"readonly" | "declare" | "typeset" | "export"
) {
for arg in &args {
if let Some(eq_pos) = arg.find('=') {
let name_part = &arg[..eq_pos];
if !name_part.is_empty()
&& !name_part.starts_with('-')
&& name_part
.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '+')
{
trace.push_str(&format!("{ps4}{arg}\n"));
}
}
}
}
result.stderr = format!("{trace}{}", result.stderr);
}
apply_output_redirects(&redirects, &mut result, state)?;
for rps in &redir_proc_subs {
if matches!(rps.kind, ast::ProcessSubstitutionKind::Write) {
let content = state
.fs
.read_file(Path::new(&rps.temp_path))
.map_err(|e| RustBashError::Execution(e.to_string()))?;
let stdin_data = String::from_utf8_lossy(&content).to_string();
let mut sub_state = make_proc_sub_state(state);
let inner_result = execute_compound_list(rps.list, &mut sub_state, &stdin_data)?;
state.counters.command_count = sub_state.counters.command_count;
state.counters.output_size = sub_state.counters.output_size;
state.proc_sub_counter = sub_state.proc_sub_counter;
result.stdout.push_str(&inner_result.stdout);
result.stderr.push_str(&inner_result.stderr);
}
}
for (inner_list, temp_path) in &deferred_write_subs {
let content = state
.fs
.read_file(Path::new(temp_path))
.map_err(|e| RustBashError::Execution(e.to_string()))?;
let stdin_data = String::from_utf8_lossy(&content).to_string();
let mut sub_state = make_proc_sub_state(state);
let inner_result = execute_compound_list(inner_list, &mut sub_state, &stdin_data)?;
state.counters.command_count = sub_state.counters.command_count;
state.counters.output_size = sub_state.counters.output_size;
state.proc_sub_counter = sub_state.proc_sub_counter;
result.stdout.push_str(&inner_result.stdout);
result.stderr.push_str(&inner_result.stderr);
}
Ok(result)
})();
for temp_path in &proc_sub_temps {
let _ = state.fs.remove_file(Path::new(temp_path));
}
state.proc_sub_prealloc.clear();
for (name, old_value) in saved {
match old_value {
Some(var) => {
state.env.insert(name, var);
}
None => {
state.env.remove(&name);
}
}
}
if let Ok(ref mut r) = inner_result
&& !prefix_stderr.is_empty()
{
r.stderr = format!("{prefix_stderr}{}", r.stderr);
}
inner_result
}
fn execute_compound_command(
compound: &ast::CompoundCommand,
redirects: Option<&ast::RedirectList>,
state: &mut InterpreterState,
stdin: &str,
) -> Result<ExecResult, RustBashError> {
let mut result = match compound {
ast::CompoundCommand::IfClause(if_clause) => execute_if(if_clause, state, stdin)?,
ast::CompoundCommand::ForClause(for_clause) => execute_for(for_clause, state, stdin)?,
ast::CompoundCommand::WhileClause(wc) => execute_while_until(wc, false, state, stdin)?,
ast::CompoundCommand::UntilClause(uc) => execute_while_until(uc, true, state, stdin)?,
ast::CompoundCommand::BraceGroup(bg) => execute_compound_list(&bg.list, state, stdin)?,
ast::CompoundCommand::Subshell(sub) => execute_subshell(&sub.list, state, stdin)?,
ast::CompoundCommand::CaseClause(cc) => execute_case(cc, state, stdin)?,
ast::CompoundCommand::Arithmetic(arith) => execute_arithmetic(arith, state)?,
ast::CompoundCommand::ArithmeticForClause(afc) => {
execute_arithmetic_for(afc, state, stdin)?
}
};
if !state.pending_cmdsub_stderr.is_empty() {
let cmdsub_stderr = std::mem::take(&mut state.pending_cmdsub_stderr);
result.stderr = format!("{cmdsub_stderr}{}", result.stderr);
}
if let Some(redir_list) = redirects {
let redir_refs: Vec<&ast::IoRedirect> = redir_list.0.iter().collect();
apply_output_redirects(&redir_refs, &mut result, state)?;
}
state.last_exit_code = result.exit_code;
Ok(result)
}
fn execute_if(
if_clause: &ast::IfClauseCommand,
state: &mut InterpreterState,
stdin: &str,
) -> Result<ExecResult, RustBashError> {
let mut result = ExecResult::default();
state.errexit_suppressed += 1;
let cond = execute_compound_list(&if_clause.condition, state, stdin)?;
state.errexit_suppressed -= 1;
result.stdout.push_str(&cond.stdout);
result.stderr.push_str(&cond.stderr);
if cond.exit_code == 0 {
let body = execute_compound_list(&if_clause.then, state, stdin)?;
result.stdout.push_str(&body.stdout);
result.stderr.push_str(&body.stderr);
result.exit_code = body.exit_code;
return Ok(result);
}
if let Some(elses) = &if_clause.elses {
for else_clause in elses {
if let Some(condition) = &else_clause.condition {
state.errexit_suppressed += 1;
let cond = execute_compound_list(condition, state, stdin)?;
state.errexit_suppressed -= 1;
result.stdout.push_str(&cond.stdout);
result.stderr.push_str(&cond.stderr);
if cond.exit_code == 0 {
let body = execute_compound_list(&else_clause.body, state, stdin)?;
result.stdout.push_str(&body.stdout);
result.stderr.push_str(&body.stderr);
result.exit_code = body.exit_code;
return Ok(result);
}
} else {
let body = execute_compound_list(&else_clause.body, state, stdin)?;
result.stdout.push_str(&body.stdout);
result.stderr.push_str(&body.stderr);
result.exit_code = body.exit_code;
return Ok(result);
}
}
}
result.exit_code = 0;
Ok(result)
}
fn execute_for(
for_clause: &ast::ForClauseCommand,
state: &mut InterpreterState,
stdin: &str,
) -> Result<ExecResult, RustBashError> {
use crate::interpreter::ControlFlow;
let mut result = ExecResult::default();
let var_name = &for_clause.variable_name;
if !var_name.is_empty()
&& (!var_name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_')
|| var_name.starts_with(|c: char| c.is_ascii_digit()))
{
result.stderr = format!("rust-bash: `{var_name}': not a valid identifier\n");
result.exit_code = 1;
state.last_exit_code = 1;
return Ok(result);
}
let values: Vec<String> = if let Some(words) = &for_clause.values {
let mut vals = Vec::new();
for w in words {
vals.extend(expand_word_mut(w, state)?);
}
vals
} else {
state.positional_params.clone()
};
state.loop_depth += 1;
let mut iterations: usize = 0;
for val in &values {
if state.should_exit {
break;
}
iterations += 1;
if iterations > state.limits.max_loop_iterations {
state.loop_depth -= 1;
return Err(RustBashError::LimitExceeded {
limit_name: "max_loop_iterations",
limit_value: state.limits.max_loop_iterations,
actual_value: iterations,
});
}
set_variable(state, &for_clause.variable_name, val.clone())?;
let r = execute_compound_list(&for_clause.body.list, state, stdin)?;
result.stdout.push_str(&r.stdout);
result.stderr.push_str(&r.stderr);
result.exit_code = r.exit_code;
match state.control_flow.take() {
Some(ControlFlow::Break(n)) => {
if n > 1 {
state.control_flow = Some(ControlFlow::Break(n - 1));
}
break;
}
Some(ControlFlow::Continue(n)) => {
if n > 1 {
state.control_flow = Some(ControlFlow::Continue(n - 1));
break;
}
}
Some(ret @ ControlFlow::Return(_)) => {
state.control_flow = Some(ret);
break;
}
None => {}
}
}
state.loop_depth -= 1;
Ok(result)
}
fn execute_arithmetic(
arith: &ast::ArithmeticCommand,
state: &mut InterpreterState,
) -> Result<ExecResult, RustBashError> {
let expanded =
crate::interpreter::expansion::expand_arith_expression(&arith.expr.value, state)?;
let val = crate::interpreter::arithmetic::eval_arithmetic(&expanded, state)?;
let mut result = ExecResult {
exit_code: if val != 0 { 0 } else { 1 },
..Default::default()
};
if state.shell_opts.xtrace {
let ps4 = expand_ps4(state);
result.stderr = format!(
"{ps4}(({}))\n{}",
arith.expr.value.trim_end(),
result.stderr
);
}
Ok(result)
}
fn execute_arithmetic_for(
afc: &ast::ArithmeticForClauseCommand,
state: &mut InterpreterState,
stdin: &str,
) -> Result<ExecResult, RustBashError> {
use crate::interpreter::ControlFlow;
if let Some(init) = &afc.initializer {
let expanded = crate::interpreter::expansion::expand_arith_expression(&init.value, state)?;
crate::interpreter::arithmetic::eval_arithmetic(&expanded, state)?;
}
let mut result = ExecResult::default();
let mut iterations: usize = 0;
state.loop_depth += 1;
loop {
if state.should_exit {
break;
}
iterations += 1;
if iterations > state.limits.max_loop_iterations {
state.loop_depth -= 1;
return Err(RustBashError::LimitExceeded {
limit_name: "max_loop_iterations",
limit_value: state.limits.max_loop_iterations,
actual_value: iterations,
});
}
if let Some(cond) = &afc.condition {
let expanded =
crate::interpreter::expansion::expand_arith_expression(&cond.value, state)?;
let val = crate::interpreter::arithmetic::eval_arithmetic(&expanded, state)?;
if val == 0 {
break;
}
}
let body = execute_compound_list(&afc.body.list, state, stdin)?;
result.stdout.push_str(&body.stdout);
result.stderr.push_str(&body.stderr);
result.exit_code = body.exit_code;
match state.control_flow.take() {
Some(ControlFlow::Break(n)) => {
if n > 1 {
state.control_flow = Some(ControlFlow::Break(n - 1));
}
break;
}
Some(ControlFlow::Continue(n)) => {
if n > 1 {
state.control_flow = Some(ControlFlow::Continue(n - 1));
break;
}
}
Some(ret @ ControlFlow::Return(_)) => {
state.control_flow = Some(ret);
break;
}
None => {}
}
if let Some(upd) = &afc.updater {
let expanded =
crate::interpreter::expansion::expand_arith_expression(&upd.value, state)?;
crate::interpreter::arithmetic::eval_arithmetic(&expanded, state)?;
}
}
state.loop_depth -= 1;
Ok(result)
}
fn execute_while_until(
clause: &ast::WhileOrUntilClauseCommand,
is_until: bool,
state: &mut InterpreterState,
stdin: &str,
) -> Result<ExecResult, RustBashError> {
use crate::interpreter::ControlFlow;
let mut result = ExecResult::default();
let mut iterations: usize = 0;
state.loop_depth += 1;
loop {
if state.should_exit {
break;
}
iterations += 1;
if iterations > state.limits.max_loop_iterations {
state.loop_depth -= 1;
return Err(RustBashError::LimitExceeded {
limit_name: "max_loop_iterations",
limit_value: state.limits.max_loop_iterations,
actual_value: iterations,
});
}
state.errexit_suppressed += 1;
let cond = execute_compound_list(&clause.0, state, stdin)?;
state.errexit_suppressed -= 1;
result.stdout.push_str(&cond.stdout);
result.stderr.push_str(&cond.stderr);
let should_continue = if is_until {
cond.exit_code != 0
} else {
cond.exit_code == 0
};
if !should_continue {
break;
}
let body = execute_compound_list(&clause.1.list, state, stdin)?;
result.stdout.push_str(&body.stdout);
result.stderr.push_str(&body.stderr);
result.exit_code = body.exit_code;
match state.control_flow.take() {
Some(ControlFlow::Break(n)) => {
if n > 1 {
state.control_flow = Some(ControlFlow::Break(n - 1));
}
break;
}
Some(ControlFlow::Continue(n)) => {
if n > 1 {
state.control_flow = Some(ControlFlow::Continue(n - 1));
break;
}
}
Some(ret @ ControlFlow::Return(_)) => {
state.control_flow = Some(ret);
break;
}
None => {}
}
}
state.loop_depth -= 1;
Ok(result)
}
fn execute_subshell(
list: &ast::CompoundList,
state: &mut InterpreterState,
stdin: &str,
) -> Result<ExecResult, RustBashError> {
let cloned_fs = state.fs.deep_clone();
let mut sub_state = InterpreterState {
fs: cloned_fs,
env: state.env.clone(),
cwd: state.cwd.clone(),
functions: state.functions.clone(),
last_exit_code: state.last_exit_code,
commands: clone_commands(&state.commands),
shell_opts: state.shell_opts.clone(),
shopt_opts: state.shopt_opts.clone(),
limits: state.limits.clone(),
counters: ExecutionCounters {
command_count: state.counters.command_count,
output_size: state.counters.output_size,
start_time: state.counters.start_time,
substitution_depth: state.counters.substitution_depth,
call_depth: 0,
},
network_policy: state.network_policy.clone(),
should_exit: false,
loop_depth: 0,
control_flow: None,
positional_params: state.positional_params.clone(),
shell_name: state.shell_name.clone(),
random_seed: state.random_seed,
local_scopes: Vec::new(),
in_function_depth: 0,
traps: state.traps.clone(),
in_trap: false,
errexit_suppressed: 0,
stdin_offset: 0,
dir_stack: state.dir_stack.clone(),
command_hash: state.command_hash.clone(),
aliases: state.aliases.clone(),
current_lineno: state.current_lineno,
shell_start_time: state.shell_start_time,
last_argument: state.last_argument.clone(),
call_stack: state.call_stack.clone(),
machtype: state.machtype.clone(),
hosttype: state.hosttype.clone(),
persistent_fds: state.persistent_fds.clone(),
next_auto_fd: state.next_auto_fd,
proc_sub_counter: state.proc_sub_counter,
proc_sub_prealloc: HashMap::new(),
pipe_stdin_bytes: None,
pending_cmdsub_stderr: String::new(),
};
let result = execute_compound_list(list, &mut sub_state, stdin);
state.counters.command_count = sub_state.counters.command_count;
state.counters.output_size = sub_state.counters.output_size;
let result = result?;
Ok(result)
}
fn execute_case(
case_clause: &ast::CaseClauseCommand,
state: &mut InterpreterState,
stdin: &str,
) -> Result<ExecResult, RustBashError> {
let value = expand_word_to_string_mut(&case_clause.value, state)?;
let mut result = ExecResult::default();
let mut i = 0;
let mut fall_through = false;
while i < case_clause.cases.len() {
let case_item = &case_clause.cases[i];
let matched = if fall_through {
fall_through = false;
true
} else {
let mut m = false;
for pattern_word in &case_item.patterns {
let pattern = expand_word_to_string_mut(pattern_word, state)?;
let matched_pattern = if state.shopt_opts.nocasematch {
if state.shopt_opts.extglob {
crate::interpreter::pattern::extglob_match_nocase(&pattern, &value)
} else {
crate::interpreter::pattern::glob_match_nocase(&pattern, &value)
}
} else if state.shopt_opts.extglob {
crate::interpreter::pattern::extglob_match(&pattern, &value)
} else {
crate::interpreter::pattern::glob_match(&pattern, &value)
};
if matched_pattern {
m = true;
break;
}
}
m
};
if matched {
if let Some(cmd) = &case_item.cmd {
let r = execute_compound_list(cmd, state, stdin)?;
result.stdout.push_str(&r.stdout);
result.stderr.push_str(&r.stderr);
result.exit_code = r.exit_code;
}
match case_item.post_action {
ast::CaseItemPostAction::ExitCase => break,
ast::CaseItemPostAction::UnconditionallyExecuteNextCaseItem => {
fall_through = true;
i += 1;
continue;
}
ast::CaseItemPostAction::ContinueEvaluatingCases => {
i += 1;
continue;
}
}
}
i += 1;
}
Ok(result)
}
pub(crate) fn clone_commands(
commands: &HashMap<String, Arc<dyn crate::commands::VirtualCommand>>,
) -> HashMap<String, Arc<dyn crate::commands::VirtualCommand>> {
commands.clone()
}
fn make_exec_callback(
state: &InterpreterState,
) -> impl Fn(&str) -> Result<CommandResult, RustBashError> {
let cloned_fs = state.fs.deep_clone();
let env = state.env.clone();
let cwd = state.cwd.clone();
let functions = state.functions.clone();
let last_exit_code = state.last_exit_code;
let commands = clone_commands(&state.commands);
let shell_opts = state.shell_opts.clone();
let shopt_opts = state.shopt_opts.clone();
let limits = state.limits.clone();
let network_policy = state.network_policy.clone();
let positional_params = state.positional_params.clone();
let shell_name = state.shell_name.clone();
let random_seed = state.random_seed;
let start_time = state.counters.start_time;
let shell_start_time = state.shell_start_time;
let last_argument = state.last_argument.clone();
let call_stack = state.call_stack.clone();
let machtype = state.machtype.clone();
let hosttype = state.hosttype.clone();
move |cmd_str: &str| {
let program = parse(cmd_str)?;
let sub_fs = cloned_fs.deep_clone();
let mut sub_state = InterpreterState {
fs: sub_fs,
env: env.clone(),
cwd: cwd.clone(),
functions: functions.clone(),
last_exit_code,
commands: clone_commands(&commands),
shell_opts: shell_opts.clone(),
shopt_opts: shopt_opts.clone(),
limits: limits.clone(),
counters: ExecutionCounters {
command_count: 0,
output_size: 0,
start_time,
substitution_depth: 0,
call_depth: 0,
},
network_policy: network_policy.clone(),
should_exit: false,
loop_depth: 0,
control_flow: None,
positional_params: positional_params.clone(),
shell_name: shell_name.clone(),
random_seed,
local_scopes: Vec::new(),
in_function_depth: 0,
traps: HashMap::new(),
in_trap: false,
errexit_suppressed: 0,
stdin_offset: 0,
dir_stack: Vec::new(),
command_hash: HashMap::new(),
aliases: HashMap::new(),
current_lineno: 0,
shell_start_time,
last_argument: last_argument.clone(),
call_stack: call_stack.clone(),
machtype: machtype.clone(),
hosttype: hosttype.clone(),
persistent_fds: HashMap::new(),
next_auto_fd: 10,
proc_sub_counter: 0,
proc_sub_prealloc: HashMap::new(),
pipe_stdin_bytes: None,
pending_cmdsub_stderr: String::new(),
};
let result = execute_program(&program, &mut sub_state)?;
Ok(CommandResult {
stdout: result.stdout,
stderr: result.stderr,
exit_code: result.exit_code,
stdout_bytes: None,
})
}
}
fn execute_function_call(
name: &str,
args: &[String],
state: &mut InterpreterState,
) -> Result<ExecResult, RustBashError> {
use crate::interpreter::ControlFlow;
state.counters.call_depth += 1;
if state.counters.call_depth > state.limits.max_call_depth {
let actual = state.counters.call_depth;
state.counters.call_depth -= 1;
return Err(RustBashError::LimitExceeded {
limit_name: "max_call_depth",
limit_value: state.limits.max_call_depth,
actual_value: actual,
});
}
let func_def = state.functions.get(name).unwrap().clone();
let saved_params = std::mem::replace(&mut state.positional_params, args.to_vec());
state.call_stack.push(CallFrame {
func_name: name.to_string(),
source: String::new(),
lineno: state.current_lineno,
});
state.local_scopes.push(HashMap::new());
state.in_function_depth += 1;
let result = execute_compound_command(&func_def.body.0, func_def.body.1.as_ref(), state, "");
let exit_code = match state.control_flow.take() {
Some(ControlFlow::Return(code)) => code,
Some(other) => {
state.control_flow = Some(other);
result.as_ref().map(|r| r.exit_code).unwrap_or(1)
}
None => result.as_ref().map(|r| r.exit_code).unwrap_or(1),
};
state.call_stack.pop();
state.in_function_depth -= 1;
if let Some(restore_map) = state.local_scopes.pop() {
for (var_name, old_value) in restore_map {
match old_value {
Some(var) => {
state.env.insert(var_name, var);
}
None => {
state.env.remove(&var_name);
}
}
}
}
state.positional_params = saved_params;
state.counters.call_depth -= 1;
let mut result = result?;
result.exit_code = exit_code;
Ok(result)
}
fn dispatch_command(
name: &str,
args: &[String],
state: &mut InterpreterState,
stdin: &str,
) -> Result<ExecResult, RustBashError> {
state.counters.command_count += 1;
check_limits(state)?;
if args.first().map(|a| a.as_str()) == Some("--help") {
if let Some(meta) = builtins::builtin_meta(name)
&& meta.supports_help_flag
{
return Ok(ExecResult {
stdout: crate::commands::format_help(meta),
stderr: String::new(),
exit_code: 0,
stdout_bytes: None,
});
}
if let Some(cmd) = state.commands.get(name)
&& let Some(meta) = cmd.meta()
&& meta.supports_help_flag
{
return Ok(ExecResult {
stdout: crate::commands::format_help(meta),
stderr: String::new(),
exit_code: 0,
stdout_bytes: None,
});
}
}
if let Some(result) = builtins::execute_builtin(name, args, state, stdin)? {
return Ok(result);
}
if state.functions.contains_key(name) {
return execute_function_call(name, args, state);
}
if let Some(cmd) = state.commands.get(name) {
let env: HashMap<String, String> = state
.env
.iter()
.map(|(k, v)| (k.clone(), v.value.as_scalar().to_string()))
.collect();
let vars_clone = state.env.clone();
let fs = Arc::clone(&state.fs);
let cwd = state.cwd.clone();
let limits = state.limits.clone();
let network_policy = state.network_policy.clone();
let binary_stdin = state.pipe_stdin_bytes.take();
let exec_callback = make_exec_callback(state);
let ctx = CommandContext {
fs: &*fs,
cwd: &cwd,
env: &env,
variables: Some(&vars_clone),
stdin,
stdin_bytes: binary_stdin.as_deref(),
limits: &limits,
network_policy: &network_policy,
exec: Some(&exec_callback),
shell_opts: Some(&state.shell_opts),
};
let effective_args: Vec<String>;
let cmd_args: &[String] = if name == "echo" && state.shopt_opts.xpg_echo {
effective_args = std::iter::once("-e".to_string())
.chain(args.iter().cloned())
.collect();
&effective_args
} else {
args
};
let cmd_result = cmd.execute(cmd_args, &ctx);
return Ok(ExecResult {
stdout: cmd_result.stdout,
stderr: cmd_result.stderr,
exit_code: cmd_result.exit_code,
stdout_bytes: cmd_result.stdout_bytes,
});
}
Ok(ExecResult {
stdout: String::new(),
stderr: format!("{name}: command not found\n"),
exit_code: 127,
stdout_bytes: None,
})
}
fn extract_fd_varname(arg: &str) -> Option<&str> {
let trimmed = arg.strip_prefix('{')?.strip_suffix('}')?;
if !trimmed.is_empty()
&& trimmed
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
&& trimmed
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_')
{
Some(trimmed)
} else {
None
}
}
fn execute_exec_builtin(
args: &[String],
redirects: &[&ast::IoRedirect],
state: &mut InterpreterState,
stdin: &str,
) -> Result<ExecResult, RustBashError> {
if let Some(first_arg) = args.first()
&& let Some(varname) = extract_fd_varname(first_arg)
{
return exec_fd_variable_alloc(varname, args.get(1..), redirects, state);
}
if args.is_empty() {
return exec_persistent_redirects(redirects, state);
}
let effective_stdin = match get_stdin_from_redirects(redirects, state, stdin) {
Ok(s) => s,
Err(RustBashError::RedirectFailed(msg)) => {
let result = ExecResult {
stderr: format!("rust-bash: {msg}\n"),
exit_code: 1,
..ExecResult::default()
};
state.last_exit_code = 1;
state.should_exit = true;
return Ok(result);
}
Err(e) => return Err(e),
};
let mut result = dispatch_command(&args[0], &args[1..], state, &effective_stdin)?;
apply_output_redirects(redirects, &mut result, state)?;
state.last_exit_code = result.exit_code;
state.should_exit = true;
Ok(result)
}
fn exec_persistent_redirects(
redirects: &[&ast::IoRedirect],
state: &mut InterpreterState,
) -> Result<ExecResult, RustBashError> {
for redir in redirects {
match redir {
ast::IoRedirect::File(fd, kind, target) => {
let filename = match redirect_target_filename(target, state) {
Ok(f) => f,
Err(RustBashError::RedirectFailed(msg)) => {
return Ok(ExecResult {
stderr: format!("rust-bash: {msg}\n"),
exit_code: 1,
..ExecResult::default()
});
}
Err(e) => return Err(e),
};
let path = resolve_path(&state.cwd, &filename);
match kind {
ast::IoFileRedirectKind::Write | ast::IoFileRedirectKind::Clobber => {
let fd_num = fd.unwrap_or(1);
if is_dev_null(&path) {
state.persistent_fds.insert(fd_num, PersistentFd::DevNull);
} else if is_dev_stdout(&path) {
state.persistent_fds.remove(&fd_num);
} else if is_dev_stderr(&path) {
state.persistent_fds.remove(&fd_num);
} else {
state
.fs
.write_file(Path::new(&path), b"")
.map_err(|e| RustBashError::Execution(e.to_string()))?;
state
.persistent_fds
.insert(fd_num, PersistentFd::OutputFile(path));
}
}
ast::IoFileRedirectKind::Append => {
let fd_num = fd.unwrap_or(1);
if is_dev_null(&path) {
state.persistent_fds.insert(fd_num, PersistentFd::DevNull);
} else if is_dev_stdout(&path) || is_dev_stderr(&path) {
state.persistent_fds.remove(&fd_num);
} else {
state
.persistent_fds
.insert(fd_num, PersistentFd::OutputFile(path));
}
}
ast::IoFileRedirectKind::Read => {
let fd_num = fd.unwrap_or(0);
if is_dev_null(&path) {
state.persistent_fds.insert(fd_num, PersistentFd::DevNull);
} else {
state
.persistent_fds
.insert(fd_num, PersistentFd::InputFile(path));
}
}
ast::IoFileRedirectKind::ReadAndWrite => {
let fd_num = fd.unwrap_or(0);
if !state.fs.exists(Path::new(&path)) {
state
.fs
.write_file(Path::new(&path), b"")
.map_err(|e| RustBashError::Execution(e.to_string()))?;
}
state
.persistent_fds
.insert(fd_num, PersistentFd::ReadWriteFile(path));
}
ast::IoFileRedirectKind::DuplicateOutput => {
let fd_num = fd.unwrap_or(1);
let dup_target = redirect_target_filename(target, state)?;
if dup_target == "-" {
state.persistent_fds.insert(fd_num, PersistentFd::Closed);
} else if let Some(stripped) = dup_target.strip_suffix('-') {
if let Ok(source_fd) = stripped.parse::<i32>() {
if let Some(entry) = state.persistent_fds.get(&source_fd).cloned() {
state.persistent_fds.insert(fd_num, entry);
}
state.persistent_fds.insert(source_fd, PersistentFd::Closed);
}
} else if let Ok(target_fd) = dup_target.parse::<i32>() {
if let Some(entry) = state.persistent_fds.get(&target_fd).cloned() {
state.persistent_fds.insert(fd_num, entry);
} else if target_fd == 0 || target_fd == 1 || target_fd == 2 {
state
.persistent_fds
.insert(fd_num, PersistentFd::DupStdFd(target_fd));
} else {
state.persistent_fds.remove(&fd_num);
}
}
}
ast::IoFileRedirectKind::DuplicateInput => {
let fd_num = fd.unwrap_or(0);
let dup_target = redirect_target_filename(target, state)?;
if dup_target == "-" {
state.persistent_fds.insert(fd_num, PersistentFd::Closed);
}
}
}
}
ast::IoRedirect::OutputAndError(word, _append) => {
let filename = expand_word_to_string_mut(word, state)?;
let path = resolve_path(&state.cwd, &filename);
if is_dev_null(&path) {
state.persistent_fds.insert(1, PersistentFd::DevNull);
state.persistent_fds.insert(2, PersistentFd::DevNull);
} else {
let pfd = PersistentFd::OutputFile(path);
state.persistent_fds.insert(1, pfd.clone());
state.persistent_fds.insert(2, pfd);
}
}
_ => {}
}
}
Ok(ExecResult::default())
}
fn exec_fd_variable_alloc(
varname: &str,
extra_args: Option<&[String]>,
redirects: &[&ast::IoRedirect],
state: &mut InterpreterState,
) -> Result<ExecResult, RustBashError> {
let is_close = redirects.iter().any(|r| {
matches!(
r,
ast::IoRedirect::File(_, ast::IoFileRedirectKind::DuplicateOutput, ast::IoFileRedirectTarget::Duplicate(w)) if w.value == "-"
)
});
if is_close {
if let Some(var) = state.env.get(varname)
&& let Ok(fd_num) = var.value.as_scalar().parse::<i32>()
{
state.persistent_fds.insert(fd_num, PersistentFd::Closed);
}
return Ok(ExecResult::default());
}
if extra_args.is_some_and(|a| !a.is_empty()) {
return Ok(ExecResult {
stderr: "rust-bash: exec: too many arguments\n".to_string(),
exit_code: 1,
..Default::default()
});
}
let fd_num = state.next_auto_fd;
state.next_auto_fd += 1;
set_variable(state, varname, fd_num.to_string())?;
for redir in redirects {
if let ast::IoRedirect::File(_fd, kind, target) = redir {
let filename = redirect_target_filename(target, state)?;
let path = resolve_path(&state.cwd, &filename);
match kind {
ast::IoFileRedirectKind::Write | ast::IoFileRedirectKind::Clobber => {
if is_dev_null(&path) {
state.persistent_fds.insert(fd_num, PersistentFd::DevNull);
} else if is_dev_stdout(&path) || is_dev_stderr(&path) {
state.persistent_fds.remove(&fd_num);
} else {
state
.fs
.write_file(Path::new(&path), b"")
.map_err(|e| RustBashError::Execution(e.to_string()))?;
state
.persistent_fds
.insert(fd_num, PersistentFd::OutputFile(path));
}
}
ast::IoFileRedirectKind::Append => {
if is_dev_null(&path) {
state.persistent_fds.insert(fd_num, PersistentFd::DevNull);
} else if is_dev_stdout(&path) || is_dev_stderr(&path) {
state.persistent_fds.remove(&fd_num);
} else {
state
.persistent_fds
.insert(fd_num, PersistentFd::OutputFile(path));
}
}
ast::IoFileRedirectKind::Read => {
if is_dev_null(&path) {
state.persistent_fds.insert(fd_num, PersistentFd::DevNull);
} else {
state
.persistent_fds
.insert(fd_num, PersistentFd::InputFile(path));
}
}
ast::IoFileRedirectKind::ReadAndWrite => {
if is_dev_null(&path) {
state.persistent_fds.insert(fd_num, PersistentFd::DevNull);
} else {
if !state.fs.exists(Path::new(&path)) {
state
.fs
.write_file(Path::new(&path), b"")
.map_err(|e| RustBashError::Execution(e.to_string()))?;
}
state
.persistent_fds
.insert(fd_num, PersistentFd::ReadWriteFile(path));
}
}
_ => {}
}
break; }
}
Ok(ExecResult::default())
}
fn is_dev_stdout(path: &str) -> bool {
path == "/dev/stdout"
}
fn is_dev_stderr(path: &str) -> bool {
path == "/dev/stderr"
}
fn is_dev_stdin(path: &str) -> bool {
path == "/dev/stdin"
}
fn is_dev_zero(path: &str) -> bool {
path == "/dev/zero"
}
fn is_dev_full(path: &str) -> bool {
path == "/dev/full"
}
fn is_special_dev_path(path: &str) -> bool {
is_dev_null(path)
|| is_dev_stdout(path)
|| is_dev_stderr(path)
|| is_dev_stdin(path)
|| is_dev_zero(path)
|| is_dev_full(path)
}
fn get_stdin_from_redirects(
redirects: &[&ast::IoRedirect],
state: &mut InterpreterState,
default_stdin: &str,
) -> Result<String, RustBashError> {
for redir in redirects {
match redir {
ast::IoRedirect::File(fd, kind, target) => {
let fd_num = fd.unwrap_or(0);
if fd_num == 0
&& matches!(
kind,
ast::IoFileRedirectKind::Read | ast::IoFileRedirectKind::ReadAndWrite
)
{
let filename = redirect_target_filename(target, state)?;
let path = resolve_path(&state.cwd, &filename);
if is_dev_stdin(&path) {
return Ok(default_stdin.to_string());
}
if is_dev_null(&path) || is_dev_zero(&path) || is_dev_full(&path) {
return Ok(String::new());
}
if filename.is_empty() {
return Err(RustBashError::RedirectFailed(
": No such file or directory".to_string(),
));
}
let content = state.fs.read_file(Path::new(&path)).map_err(|_| {
RustBashError::RedirectFailed(format!(
"{filename}: No such file or directory"
))
})?;
return Ok(String::from_utf8_lossy(&content).to_string());
}
if fd_num == 0 && matches!(kind, ast::IoFileRedirectKind::DuplicateInput) {
let dup_target = redirect_target_filename(target, state)?;
if let Ok(source_fd) = dup_target.parse::<i32>()
&& let Some(pfd) = state.persistent_fds.get(&source_fd)
{
match pfd {
PersistentFd::InputFile(path) | PersistentFd::ReadWriteFile(path) => {
let content = state
.fs
.read_file(Path::new(path))
.map_err(|e| RustBashError::Execution(e.to_string()))?;
return Ok(String::from_utf8_lossy(&content).to_string());
}
PersistentFd::DevNull | PersistentFd::Closed => {
return Ok(String::new());
}
PersistentFd::OutputFile(_) | PersistentFd::DupStdFd(_) => {}
}
}
}
}
ast::IoRedirect::HereString(fd, word) => {
let fd_num = fd.unwrap_or(0);
if fd_num == 0 {
let val = expand_word_to_string_mut(word, state)?;
if val.len() > state.limits.max_heredoc_size {
return Err(RustBashError::LimitExceeded {
limit_name: "max_heredoc_size",
limit_value: state.limits.max_heredoc_size,
actual_value: val.len(),
});
}
return Ok(format!("{val}\n"));
}
}
ast::IoRedirect::HereDocument(fd, heredoc) => {
let fd_num = fd.unwrap_or(0);
if fd_num == 0 {
let body = if heredoc.requires_expansion {
expand_word_to_string_mut(&heredoc.doc, state)?
} else {
heredoc.doc.value.clone()
};
if body.len() > state.limits.max_heredoc_size {
return Err(RustBashError::LimitExceeded {
limit_name: "max_heredoc_size",
limit_value: state.limits.max_heredoc_size,
actual_value: body.len(),
});
}
if heredoc.remove_tabs {
return Ok(body
.lines()
.map(|l| l.trim_start_matches('\t'))
.collect::<Vec<_>>()
.join("\n")
+ if body.ends_with('\n') { "\n" } else { "" });
}
return Ok(body);
}
}
_ => {}
}
}
Ok(default_stdin.to_string())
}
fn apply_output_redirects(
redirects: &[&ast::IoRedirect],
result: &mut ExecResult,
state: &mut InterpreterState,
) -> Result<(), RustBashError> {
let mut redirected_fds = std::collections::HashSet::new();
let mut deferred_errors: Vec<String> = Vec::new();
for redir in redirects {
match redir {
ast::IoRedirect::File(fd, kind, target) => {
let fd_num = match kind {
ast::IoFileRedirectKind::Read
| ast::IoFileRedirectKind::ReadAndWrite
| ast::IoFileRedirectKind::DuplicateInput => fd.unwrap_or(0),
_ => fd.unwrap_or(1),
};
redirected_fds.insert(fd_num);
let cont =
apply_file_redirect(*fd, kind, target, result, state, &mut deferred_errors)?;
if !cont {
break;
}
}
ast::IoRedirect::OutputAndError(word, append) => {
redirected_fds.insert(1);
redirected_fds.insert(2);
let filename = expand_word_to_string_mut(word, state)?;
if filename.is_empty() {
result
.stderr
.push_str("rust-bash: : No such file or directory\n");
result.exit_code = 1;
break;
}
let path = resolve_path(&state.cwd, &filename);
if state.shell_opts.noclobber
&& !*append
&& !is_dev_null(&path)
&& state.fs.exists(Path::new(&path))
{
result.stderr.push_str(&format!(
"rust-bash: {filename}: cannot overwrite existing file\n"
));
result.stdout.clear();
result.exit_code = 1;
break;
}
let combined = format!("{}{}", result.stdout, result.stderr);
if is_dev_null(&path) {
result.stdout.clear();
result.stderr.clear();
} else if *append {
write_or_append(state, &path, &combined, true)?;
result.stdout.clear();
result.stderr.clear();
} else {
write_or_append(state, &path, &combined, false)?;
result.stdout.clear();
result.stderr.clear();
}
}
_ => {} }
}
apply_persistent_fd_fallback(result, state, &redirected_fds)?;
for err in deferred_errors {
result.stderr.push_str(&err);
}
Ok(())
}
fn apply_persistent_fd_fallback(
result: &mut ExecResult,
state: &InterpreterState,
redirected_fds: &std::collections::HashSet<i32>,
) -> Result<(), RustBashError> {
if !redirected_fds.contains(&1)
&& let Some(pfd) = state.persistent_fds.get(&1)
{
match pfd {
PersistentFd::OutputFile(path) => {
if !result.stdout.is_empty() {
write_or_append(state, path, &result.stdout, true)?;
result.stdout.clear();
}
}
PersistentFd::DevNull | PersistentFd::Closed => {
result.stdout.clear();
}
_ => {}
}
}
if !redirected_fds.contains(&2)
&& let Some(pfd) = state.persistent_fds.get(&2)
{
match pfd {
PersistentFd::OutputFile(path) => {
if !result.stderr.is_empty() {
write_or_append(state, path, &result.stderr, true)?;
result.stderr.clear();
}
}
PersistentFd::DevNull | PersistentFd::Closed => {
result.stderr.clear();
}
_ => {}
}
}
Ok(())
}
fn apply_file_redirect(
fd: Option<i32>,
kind: &ast::IoFileRedirectKind,
target: &ast::IoFileRedirectTarget,
result: &mut ExecResult,
state: &mut InterpreterState,
deferred_errors: &mut Vec<String>,
) -> Result<bool, RustBashError> {
macro_rules! try_filename {
($target:expr, $state:expr, $result:expr) => {
match redirect_target_filename($target, $state) {
Ok(f) => f,
Err(RustBashError::RedirectFailed(msg)) => {
let fd_num = fd.unwrap_or(1);
if fd_num == 1 {
$result.stdout.clear();
} else if fd_num == 2 {
$result.stderr.clear();
}
$result.stderr.push_str(&format!("rust-bash: {msg}\n"));
$result.exit_code = 1;
return Ok(false);
}
Err(e) => return Err(e),
}
};
}
match kind {
ast::IoFileRedirectKind::Write | ast::IoFileRedirectKind::Clobber => {
let fd_num = fd.unwrap_or(1);
let filename = try_filename!(target, state, result);
let path = resolve_path(&state.cwd, &filename);
if state.shell_opts.noclobber
&& matches!(kind, ast::IoFileRedirectKind::Write)
&& !is_dev_null(&path)
&& !is_special_dev_path(&path)
&& state.fs.exists(Path::new(&path))
{
result.stderr.push_str(&format!(
"rust-bash: {filename}: cannot overwrite existing file\n"
));
if fd_num == 1 {
result.stdout.clear();
}
result.exit_code = 1;
return Ok(false);
}
apply_write_redirect(fd_num, &path, result, state, false, deferred_errors)?;
}
ast::IoFileRedirectKind::Append => {
let fd_num = fd.unwrap_or(1);
let filename = try_filename!(target, state, result);
let path = resolve_path(&state.cwd, &filename);
apply_write_redirect(fd_num, &path, result, state, true, deferred_errors)?;
}
ast::IoFileRedirectKind::DuplicateOutput => {
let fd_num = fd.unwrap_or(1);
if !apply_duplicate_output(fd_num, target, result, state)? {
return Ok(false);
}
}
ast::IoFileRedirectKind::DuplicateInput => {
let fd_num = fd.unwrap_or(0);
if fd_num == 0 {
} else {
if !apply_duplicate_output(fd_num, target, result, state)? {
return Ok(false);
}
}
}
ast::IoFileRedirectKind::Read => {
}
ast::IoFileRedirectKind::ReadAndWrite => {
let fd_num = fd.unwrap_or(0);
let filename = try_filename!(target, state, result);
let path = resolve_path(&state.cwd, &filename);
if !state.fs.exists(Path::new(&path)) {
state
.fs
.write_file(Path::new(&path), b"")
.map_err(|e| RustBashError::Execution(e.to_string()))?;
}
if fd_num == 1 {
write_or_append(state, &path, &result.stdout, false)?;
result.stdout.clear();
} else if fd_num == 2 {
write_or_append(state, &path, &result.stderr, false)?;
result.stderr.clear();
}
}
}
Ok(true)
}
fn apply_write_redirect(
fd_num: i32,
path: &str,
result: &mut ExecResult,
state: &InterpreterState,
append: bool,
deferred_errors: &mut Vec<String>,
) -> Result<(), RustBashError> {
if is_dev_null(path) || is_dev_zero(path) {
if fd_num == 1 {
result.stdout.clear();
result.stdout_bytes = None;
} else if fd_num == 2 {
result.stderr.clear();
}
} else if is_dev_stdout(path) {
if fd_num == 2 {
result.stdout.push_str(&result.stderr);
result.stderr.clear();
}
} else if is_dev_stderr(path) {
if fd_num == 1 {
result.stderr.push_str(&result.stdout);
result.stdout.clear();
}
} else if is_dev_full(path) {
deferred_errors
.push("rust-bash: write error: /dev/full: No space left on device\n".to_string());
if fd_num == 1 {
result.stdout.clear();
} else if fd_num == 2 {
result.stderr.clear();
}
result.exit_code = 1;
} else {
let p = Path::new(path);
if state.fs.exists(p)
&& let Ok(meta) = state.fs.stat(p)
&& meta.node_type == crate::vfs::NodeType::Directory
{
let basename = path.rsplit('/').next().unwrap_or(path);
let display = if basename.is_empty() { path } else { basename };
deferred_errors.push(format!("rust-bash: {display}: Is a directory\n"));
if fd_num == 1 {
result.stdout.clear();
} else if fd_num == 2 {
result.stderr.clear();
}
result.exit_code = 1;
return Ok(());
}
let content_bytes: Vec<u8> = if fd_num == 1 {
if let Some(bytes) = result.stdout_bytes.take() {
bytes
} else {
result.stdout.as_bytes().to_vec()
}
} else if fd_num == 2 {
result.stderr.as_bytes().to_vec()
} else {
return write_to_persistent_fd(fd_num, result, state);
};
write_or_append_bytes(state, path, &content_bytes, append)?;
if fd_num == 1 {
result.stdout.clear();
result.stdout_bytes = None;
} else if fd_num == 2 {
result.stderr.clear();
}
}
Ok(())
}
fn write_to_persistent_fd(
_fd_num: i32,
_result: &mut ExecResult,
_state: &InterpreterState,
) -> Result<(), RustBashError> {
Ok(())
}
fn apply_duplicate_output(
fd_num: i32,
target: &ast::IoFileRedirectTarget,
result: &mut ExecResult,
state: &mut InterpreterState,
) -> Result<bool, RustBashError> {
let dup_target_str = match target {
ast::IoFileRedirectTarget::Duplicate(word) => expand_word_to_string_mut(word, state)?,
ast::IoFileRedirectTarget::Fd(target_fd) => target_fd.to_string(),
_ => return Ok(true),
};
if dup_target_str == "-" {
if fd_num == 1 {
result.stdout.clear();
} else if fd_num == 2 {
result.stderr.clear();
}
return Ok(true);
}
if let Some(source_str) = dup_target_str.strip_suffix('-') {
if let Ok(source_fd) = source_str.parse::<i32>() {
apply_dup_fd(fd_num, source_fd, result, state)?;
if source_fd == 1 {
result.stdout.clear();
} else if source_fd == 2 {
result.stderr.clear();
} else {
state.persistent_fds.insert(source_fd, PersistentFd::Closed);
}
}
return Ok(true);
}
if let Ok(target_fd) = dup_target_str.parse::<i32>() {
if target_fd != 0
&& target_fd != 1
&& target_fd != 2
&& !state.persistent_fds.contains_key(&target_fd)
{
if fd_num == 1 {
result.stdout.clear();
}
result
.stderr
.push_str(&format!("rust-bash: {fd_num}: Bad file descriptor\n"));
result.exit_code = 1;
return Ok(false);
}
apply_dup_fd(fd_num, target_fd, result, state)?;
}
Ok(true)
}
fn apply_dup_fd(
fd_num: i32,
target_fd: i32,
result: &mut ExecResult,
state: &InterpreterState,
) -> Result<(), RustBashError> {
if target_fd == 1 && fd_num == 2 {
result.stdout.push_str(&result.stderr);
result.stderr.clear();
} else if target_fd == 2 && fd_num == 1 {
result.stderr.push_str(&result.stdout);
result.stdout.clear();
} else if fd_num == 1 || fd_num == 2 {
if let Some(pfd) = state.persistent_fds.get(&target_fd) {
match pfd {
PersistentFd::OutputFile(path) => {
let content = if fd_num == 1 {
let c = result.stdout.clone();
result.stdout.clear();
c
} else {
let c = result.stderr.clone();
result.stderr.clear();
c
};
write_or_append(state, path, &content, true)?;
}
PersistentFd::DevNull | PersistentFd::Closed => {
if fd_num == 1 {
result.stdout.clear();
} else {
result.stderr.clear();
}
}
PersistentFd::DupStdFd(std_fd) => {
if *std_fd == 1 && fd_num == 2 {
result.stdout.push_str(&result.stderr);
result.stderr.clear();
} else if *std_fd == 2 && fd_num == 1 {
result.stderr.push_str(&result.stdout);
result.stdout.clear();
}
}
_ => {}
}
}
}
Ok(())
}
fn expand_process_substitution<'a>(
kind: &ast::ProcessSubstitutionKind,
list: &'a ast::CompoundList,
state: &mut InterpreterState,
deferred_write_subs: &mut Vec<(&'a ast::CompoundList, String)>,
) -> Result<String, RustBashError> {
match kind {
ast::ProcessSubstitutionKind::Read => execute_read_process_substitution(list, state),
ast::ProcessSubstitutionKind::Write => {
let path = allocate_proc_sub_temp_file(state, b"")?;
deferred_write_subs.push((list, path.clone()));
Ok(path)
}
}
}
fn redirect_target_filename(
target: &ast::IoFileRedirectTarget,
state: &mut InterpreterState,
) -> Result<String, RustBashError> {
match target {
ast::IoFileRedirectTarget::Filename(word) => {
let filename = expand_word_to_string_mut(word, state)?;
if filename.is_empty() {
return Err(RustBashError::RedirectFailed(
": No such file or directory".to_string(),
));
}
Ok(filename)
}
ast::IoFileRedirectTarget::Fd(fd) => Ok(fd.to_string()),
ast::IoFileRedirectTarget::Duplicate(word) => expand_word_to_string_mut(word, state),
ast::IoFileRedirectTarget::ProcessSubstitution(_, _) => {
let key = std::ptr::from_ref(target) as usize;
state.proc_sub_prealloc.remove(&key).ok_or_else(|| {
RustBashError::Execution(
"process substitution: no pre-allocated path available".into(),
)
})
}
}
}
fn execute_read_process_substitution(
list: &ast::CompoundList,
state: &mut InterpreterState,
) -> Result<String, RustBashError> {
let mut sub_state = make_proc_sub_state(state);
let result = execute_compound_list(list, &mut sub_state, "")?;
state.counters.command_count = sub_state.counters.command_count;
state.counters.output_size = sub_state.counters.output_size;
state.proc_sub_counter = sub_state.proc_sub_counter;
allocate_proc_sub_temp_file(state, result.stdout.as_bytes())
}
fn allocate_proc_sub_temp_file(
state: &mut InterpreterState,
content: &[u8],
) -> Result<String, RustBashError> {
let path = format!("/tmp/.proc_sub_{}", state.proc_sub_counter);
state.proc_sub_counter += 1;
let tmp = Path::new("/tmp");
if !state.fs.exists(tmp) {
state
.fs
.mkdir_p(tmp)
.map_err(|e| RustBashError::Execution(e.to_string()))?;
}
state
.fs
.write_file(Path::new(&path), content)
.map_err(|e| RustBashError::Execution(e.to_string()))?;
Ok(path)
}
fn make_proc_sub_state(state: &mut InterpreterState) -> InterpreterState {
InterpreterState {
fs: Arc::clone(&state.fs),
env: state.env.clone(),
cwd: state.cwd.clone(),
functions: state.functions.clone(),
last_exit_code: state.last_exit_code,
commands: clone_commands(&state.commands),
shell_opts: state.shell_opts.clone(),
shopt_opts: state.shopt_opts.clone(),
limits: state.limits.clone(),
counters: ExecutionCounters {
command_count: state.counters.command_count,
output_size: state.counters.output_size,
start_time: state.counters.start_time,
substitution_depth: state.counters.substitution_depth,
call_depth: 0,
},
network_policy: state.network_policy.clone(),
should_exit: false,
loop_depth: 0,
control_flow: None,
positional_params: state.positional_params.clone(),
shell_name: state.shell_name.clone(),
random_seed: state.random_seed,
local_scopes: Vec::new(),
in_function_depth: 0,
traps: HashMap::new(),
in_trap: false,
errexit_suppressed: 0,
stdin_offset: 0,
dir_stack: state.dir_stack.clone(),
command_hash: state.command_hash.clone(),
aliases: state.aliases.clone(),
current_lineno: state.current_lineno,
shell_start_time: state.shell_start_time,
last_argument: state.last_argument.clone(),
call_stack: state.call_stack.clone(),
machtype: state.machtype.clone(),
hosttype: state.hosttype.clone(),
persistent_fds: HashMap::new(),
next_auto_fd: 10,
proc_sub_counter: state.proc_sub_counter,
proc_sub_prealloc: HashMap::new(),
pipe_stdin_bytes: None,
pending_cmdsub_stderr: String::new(),
}
}
fn is_dev_null(path: &str) -> bool {
path == "/dev/null"
}
fn write_or_append(
state: &InterpreterState,
path: &str,
content: &str,
append: bool,
) -> Result<(), RustBashError> {
write_or_append_bytes(state, path, content.as_bytes(), append)
}
fn write_or_append_bytes(
state: &InterpreterState,
path: &str,
content: &[u8],
append: bool,
) -> Result<(), RustBashError> {
let p = Path::new(path);
if append {
if state.fs.exists(p) {
state
.fs
.append_file(p, content)
.map_err(|e| RustBashError::Execution(e.to_string()))?;
} else {
state
.fs
.write_file(p, content)
.map_err(|e| RustBashError::Execution(e.to_string()))?;
}
} else {
state
.fs
.write_file(p, content)
.map_err(|e| RustBashError::Execution(e.to_string()))?;
}
Ok(())
}
fn execute_extended_test(
expr: &ast::ExtendedTestExpr,
state: &mut InterpreterState,
) -> Result<ExecResult, RustBashError> {
let should_trace = state.shell_opts.xtrace;
let mut exec_result = match eval_extended_test_expr(expr, state) {
Ok(result) => ExecResult {
exit_code: if result { 0 } else { 1 },
..ExecResult::default()
},
Err(RustBashError::Execution(ref msg)) => {
let exit_code = if msg.contains("invalid regex") { 2 } else { 1 };
state.last_exit_code = exit_code;
ExecResult {
stderr: format!("rust-bash: {msg}\n"),
exit_code,
..ExecResult::default()
}
}
Err(e) => return Err(e),
};
if should_trace {
let repr = format_extended_test_expr_expanded(expr, state);
let ps4 = expand_ps4(state);
exec_result.stderr = format!("{ps4}[[ {repr} ]]\n{}", exec_result.stderr);
}
Ok(exec_result)
}
fn format_extended_test_expr_expanded(
expr: &ast::ExtendedTestExpr,
state: &mut InterpreterState,
) -> String {
match expr {
ast::ExtendedTestExpr::And(l, r) => {
format!(
"{} && {}",
format_extended_test_expr_expanded(l, state),
format_extended_test_expr_expanded(r, state)
)
}
ast::ExtendedTestExpr::Or(l, r) => {
format!(
"{} || {}",
format_extended_test_expr_expanded(l, state),
format_extended_test_expr_expanded(r, state)
)
}
ast::ExtendedTestExpr::Not(inner) => {
format!("! {}", format_extended_test_expr_expanded(inner, state))
}
ast::ExtendedTestExpr::Parenthesized(inner) => {
format_extended_test_expr_expanded(inner, state)
}
ast::ExtendedTestExpr::UnaryTest(pred, word) => {
let expanded = expand_word_to_string_mut(word, state).unwrap_or_default();
format!("{} {}", format_unary_pred(pred), expanded)
}
ast::ExtendedTestExpr::BinaryTest(pred, l, r) => {
let l_exp = expand_word_to_string_mut(l, state).unwrap_or_default();
let r_exp = expand_word_to_string_mut(r, state).unwrap_or_default();
format!("{} {} {}", l_exp, format_binary_pred(pred), r_exp)
}
}
}
fn format_unary_pred(pred: &ast::UnaryPredicate) -> &'static str {
use brush_parser::ast::UnaryPredicate;
match pred {
UnaryPredicate::FileExists => "-a",
UnaryPredicate::FileExistsAndIsBlockSpecialFile => "-b",
UnaryPredicate::FileExistsAndIsCharSpecialFile => "-c",
UnaryPredicate::FileExistsAndIsDir => "-d",
UnaryPredicate::FileExistsAndIsRegularFile => "-f",
UnaryPredicate::FileExistsAndIsSetgid => "-g",
UnaryPredicate::FileExistsAndIsSymlink => "-h",
UnaryPredicate::FileExistsAndHasStickyBit => "-k",
UnaryPredicate::FileExistsAndIsFifo => "-p",
UnaryPredicate::FileExistsAndIsReadable => "-r",
UnaryPredicate::FileExistsAndIsNotZeroLength => "-s",
UnaryPredicate::FdIsOpenTerminal => "-t",
UnaryPredicate::FileExistsAndIsSetuid => "-u",
UnaryPredicate::FileExistsAndIsWritable => "-w",
UnaryPredicate::FileExistsAndIsExecutable => "-x",
UnaryPredicate::FileExistsAndOwnedByEffectiveGroupId => "-G",
UnaryPredicate::FileExistsAndModifiedSinceLastRead => "-N",
UnaryPredicate::FileExistsAndOwnedByEffectiveUserId => "-O",
UnaryPredicate::FileExistsAndIsSocket => "-S",
UnaryPredicate::StringHasZeroLength => "-z",
UnaryPredicate::StringHasNonZeroLength => "-n",
UnaryPredicate::ShellOptionEnabled => "-o",
UnaryPredicate::ShellVariableIsSetAndAssigned => "-v",
UnaryPredicate::ShellVariableIsSetAndNameRef => "-R",
}
}
fn format_binary_pred(pred: &ast::BinaryPredicate) -> &'static str {
use brush_parser::ast::BinaryPredicate;
match pred {
BinaryPredicate::StringExactlyMatchesPattern => "==",
BinaryPredicate::StringDoesNotExactlyMatchPattern => "!=",
BinaryPredicate::StringExactlyMatchesString => "==",
BinaryPredicate::StringDoesNotExactlyMatchString => "!=",
BinaryPredicate::StringMatchesRegex => "=~",
BinaryPredicate::StringContainsSubstring => "=~",
BinaryPredicate::ArithmeticEqualTo => "-eq",
BinaryPredicate::ArithmeticNotEqualTo => "-ne",
BinaryPredicate::ArithmeticLessThan => "-lt",
BinaryPredicate::ArithmeticGreaterThan => "-gt",
BinaryPredicate::ArithmeticLessThanOrEqualTo => "-le",
BinaryPredicate::ArithmeticGreaterThanOrEqualTo => "-ge",
BinaryPredicate::FilesReferToSameDeviceAndInodeNumbers => "-ef",
BinaryPredicate::LeftFileIsNewerOrExistsWhenRightDoesNot => "-nt",
BinaryPredicate::LeftFileIsOlderOrDoesNotExistWhenRightDoes => "-ot",
_ => "?",
}
}
fn eval_extended_test_expr(
expr: &ast::ExtendedTestExpr,
state: &mut InterpreterState,
) -> Result<bool, RustBashError> {
match expr {
ast::ExtendedTestExpr::And(left, right) => {
let l = eval_extended_test_expr(left, state)?;
if !l {
return Ok(false);
}
eval_extended_test_expr(right, state)
}
ast::ExtendedTestExpr::Or(left, right) => {
let l = eval_extended_test_expr(left, state)?;
if l {
return Ok(true);
}
eval_extended_test_expr(right, state)
}
ast::ExtendedTestExpr::Not(inner) => {
let val = eval_extended_test_expr(inner, state)?;
Ok(!val)
}
ast::ExtendedTestExpr::Parenthesized(inner) => eval_extended_test_expr(inner, state),
ast::ExtendedTestExpr::UnaryTest(pred, word) => {
use brush_parser::ast::UnaryPredicate;
if matches!(pred, UnaryPredicate::ShellVariableIsSetAndAssigned) {
let operand = expand_word_to_string_mut(word, state)?;
return Ok(test_variable_is_set(&operand, state));
}
let operand = expand_word_to_string_mut(word, state)?;
let env: HashMap<String, String> = state
.env
.iter()
.map(|(k, v)| (k.clone(), v.value.as_scalar().to_string()))
.collect();
Ok(crate::commands::test_cmd::eval_unary_predicate(
pred,
&operand,
&*state.fs,
&state.cwd,
&env,
Some(&state.shell_opts),
))
}
ast::ExtendedTestExpr::BinaryTest(pred, left_word, right_word) => {
let left = expand_word_to_string_mut(left_word, state)?;
if matches!(
pred,
ast::BinaryPredicate::StringMatchesRegex
| ast::BinaryPredicate::StringContainsSubstring
) {
let raw = &right_word.value;
let is_fully_quoted = is_word_fully_quoted(raw);
let pattern = expand_word_to_string_mut(right_word, state)?;
if is_fully_quoted {
return Ok(left.contains(&pattern));
}
let effective_pattern = build_regex_with_quoted_literals(raw, state)?;
return eval_regex_match(&left, &effective_pattern, state);
}
let right = expand_word_to_string_mut(right_word, state)?;
use brush_parser::ast::BinaryPredicate;
if matches!(
pred,
BinaryPredicate::ArithmeticEqualTo
| BinaryPredicate::ArithmeticNotEqualTo
| BinaryPredicate::ArithmeticLessThan
| BinaryPredicate::ArithmeticGreaterThan
| BinaryPredicate::ArithmeticLessThanOrEqualTo
| BinaryPredicate::ArithmeticGreaterThanOrEqualTo
) {
let lval =
crate::commands::test_cmd::parse_bash_int_pub(&left).unwrap_or_else(|| {
crate::interpreter::arithmetic::eval_arithmetic(&left, state).unwrap_or(0)
});
let rval =
crate::commands::test_cmd::parse_bash_int_pub(&right).unwrap_or_else(|| {
crate::interpreter::arithmetic::eval_arithmetic(&right, state).unwrap_or(0)
});
let result = match pred {
BinaryPredicate::ArithmeticEqualTo => lval == rval,
BinaryPredicate::ArithmeticNotEqualTo => lval != rval,
BinaryPredicate::ArithmeticLessThan => lval < rval,
BinaryPredicate::ArithmeticGreaterThan => lval > rval,
BinaryPredicate::ArithmeticLessThanOrEqualTo => lval <= rval,
BinaryPredicate::ArithmeticGreaterThanOrEqualTo => lval >= rval,
_ => unreachable!(),
};
return Ok(result);
}
if state.shopt_opts.nocasematch {
let result = match pred {
ast::BinaryPredicate::StringExactlyMatchesPattern => {
crate::interpreter::pattern::extglob_match_nocase(&right, &left)
}
ast::BinaryPredicate::StringDoesNotExactlyMatchPattern => {
!crate::interpreter::pattern::extglob_match_nocase(&right, &left)
}
ast::BinaryPredicate::StringExactlyMatchesString => {
left.eq_ignore_ascii_case(&right)
}
ast::BinaryPredicate::StringDoesNotExactlyMatchString => {
!left.eq_ignore_ascii_case(&right)
}
_ => crate::commands::test_cmd::eval_binary_predicate(
pred, &left, &right, true, &*state.fs, &state.cwd,
),
};
Ok(result)
} else {
let result = match pred {
ast::BinaryPredicate::StringExactlyMatchesPattern => {
crate::interpreter::pattern::extglob_match(&right, &left)
}
ast::BinaryPredicate::StringDoesNotExactlyMatchPattern => {
!crate::interpreter::pattern::extglob_match(&right, &left)
}
_ => crate::commands::test_cmd::eval_binary_predicate(
pred, &left, &right, true, &*state.fs, &state.cwd,
),
};
Ok(result)
}
}
}
}
fn test_variable_is_set(operand: &str, state: &mut InterpreterState) -> bool {
if let Some(bracket_pos) = operand.find('[')
&& operand.ends_with(']')
{
let name = &operand[..bracket_pos];
let index = &operand[bracket_pos + 1..operand.len() - 1];
let resolved = crate::interpreter::resolve_nameref_or_self(name, state);
if index == "@" || index == "*" {
return state
.env
.get(&resolved)
.is_some_and(|var| match &var.value {
VariableValue::IndexedArray(map) => !map.is_empty(),
VariableValue::AssociativeArray(map) => !map.is_empty(),
_ => false,
});
}
let var_type = state.env.get(&resolved).map(|var| match &var.value {
VariableValue::IndexedArray(_) => 0,
VariableValue::AssociativeArray(_) => 1,
VariableValue::Scalar(_) => 2,
});
return match var_type {
Some(0) => {
let idx = eval_index_arithmetic(index, state);
let Some(var) = state.env.get(&resolved) else {
return false;
};
if let VariableValue::IndexedArray(map) = &var.value {
let actual_idx = if idx < 0 {
let max_key = map.keys().next_back().copied().unwrap_or(0);
let resolved_idx = max_key as i64 + 1 + idx;
if resolved_idx < 0 {
return false;
}
resolved_idx as usize
} else {
idx as usize
};
map.contains_key(&actual_idx)
} else {
false
}
}
Some(1) => {
state
.env
.get(&resolved)
.and_then(|var| {
if let VariableValue::AssociativeArray(map) = &var.value {
Some(map.contains_key(index))
} else {
None
}
})
.unwrap_or(false)
}
Some(2) => {
let idx = eval_index_arithmetic(index, state);
idx == 0 || idx == -1
}
_ => false,
};
}
let resolved = crate::interpreter::resolve_nameref_or_self(operand, state);
state.env.contains_key(&resolved)
}
fn eval_index_arithmetic(index: &str, state: &mut InterpreterState) -> i64 {
crate::interpreter::arithmetic::eval_arithmetic(index, state)
.unwrap_or_else(|_| crate::interpreter::expansion::simple_arith_eval(index, state))
}
fn eval_regex_match(
string: &str,
pattern: &str,
state: &mut InterpreterState,
) -> Result<bool, RustBashError> {
let effective_pattern = if state.shopt_opts.nocasematch {
format!("(?i){pattern}")
} else {
pattern.to_string()
};
let re = regex::Regex::new(&effective_pattern)
.map_err(|e| RustBashError::Execution(format!("invalid regex '{pattern}': {e}")))?;
if let Some(captures) = re.captures(string) {
let mut map = std::collections::BTreeMap::new();
let whole = captures.get(0).map(|m| m.as_str()).unwrap_or("");
map.insert(0, whole.to_string());
for i in 1..captures.len() {
let val = captures.get(i).map(|m| m.as_str()).unwrap_or("");
map.insert(i, val.to_string());
}
state.env.insert(
"BASH_REMATCH".to_string(),
Variable {
value: VariableValue::IndexedArray(map),
attrs: VariableAttrs::empty(),
},
);
Ok(true)
} else {
state.env.insert(
"BASH_REMATCH".to_string(),
Variable {
value: VariableValue::IndexedArray(std::collections::BTreeMap::new()),
attrs: VariableAttrs::empty(),
},
);
Ok(false)
}
}
fn is_word_fully_quoted(raw: &str) -> bool {
let trimmed = raw.trim();
if trimmed.len() < 2 {
return false;
}
if trimmed.starts_with('\'') && trimmed.ends_with('\'') {
return true;
}
if trimmed.starts_with('"') && trimmed.ends_with('"') {
return true;
}
if (trimmed.starts_with("$'") && trimmed.ends_with('\''))
|| (trimmed.starts_with("$\"") && trimmed.ends_with('"'))
{
return true;
}
false
}
fn build_regex_with_quoted_literals(
raw: &str,
state: &mut InterpreterState,
) -> Result<String, RustBashError> {
let mut result = String::new();
let chars: Vec<char> = raw.chars().collect();
let mut i = 0;
while i < chars.len() {
match chars[i] {
'\'' => {
i += 1;
let mut literal = String::new();
while i < chars.len() && chars[i] != '\'' {
literal.push(chars[i]);
i += 1;
}
if i < chars.len() {
i += 1; }
result.push_str(®ex::escape(&literal));
}
'"' => {
i += 1;
let mut content = String::new();
while i < chars.len() && chars[i] != '"' {
if chars[i] == '\\' && i + 1 < chars.len() {
content.push(chars[i + 1]);
i += 2;
} else {
content.push(chars[i]);
i += 1;
}
}
if i < chars.len() {
i += 1; }
let word = ast::Word {
value: content,
loc: None,
};
let expanded = expand_word_to_string_mut(&word, state)?;
result.push_str(®ex::escape(&expanded));
}
'\\' if i + 1 < chars.len() => {
result.push_str(®ex::escape(&chars[i + 1].to_string()));
i += 2;
}
'$' => {
let mut var_text = String::new();
var_text.push('$');
i += 1;
if i < chars.len() && chars[i] == '{' {
var_text.push('{');
i += 1;
let mut depth = 1;
while i < chars.len() && depth > 0 {
if chars[i] == '{' {
depth += 1;
} else if chars[i] == '}' {
depth -= 1;
}
var_text.push(chars[i]);
i += 1;
}
} else {
while i < chars.len() && (chars[i].is_ascii_alphanumeric() || chars[i] == '_') {
var_text.push(chars[i]);
i += 1;
}
}
let word = ast::Word {
value: var_text,
loc: None,
};
let expanded = expand_word_to_string_mut(&word, state)?;
result.push_str(&expanded);
}
c => {
result.push(c);
i += 1;
}
}
}
Ok(result)
}