1use crate::commands::{CommandContext, CommandResult};
4use crate::error::RustBashError;
5use crate::interpreter::builtins::{self, resolve_path};
6use crate::interpreter::expansion::{expand_word_mut, expand_word_to_string_mut};
7use crate::interpreter::{
8 CallFrame, ExecResult, ExecutionCounters, FunctionDef, InterpreterState, PersistentFd,
9 Variable, VariableAttrs, VariableValue, execute_trap, parse, set_array_element, set_variable,
10};
11
12use brush_parser::ast;
13use brush_parser::ast::SourceLocation;
14use std::collections::HashMap;
15use std::path::Path;
16use std::sync::Arc;
17
18fn expand_ps4(state: &mut InterpreterState) -> String {
23 let raw = state
24 .env
25 .get("PS4")
26 .map(|v| v.value.as_scalar().to_string());
27 match raw {
28 Some(s) if !s.is_empty() => {
29 let word = brush_parser::ast::Word {
30 value: s,
31 loc: Default::default(),
32 };
33 expand_word_to_string_mut(&word, state).unwrap_or_else(|_| "+ ".to_string())
34 }
35 Some(_) => "+ ".to_string(), None => String::new(), }
38}
39
40fn xtrace_quote(word: &str) -> String {
45 if word.is_empty() {
46 return "''".to_string();
47 }
48
49 let has_single_quote = word.contains('\'');
51 let needs_quoting = word
52 .chars()
53 .any(|c| c.is_whitespace() || c == '\'' || c == '"' || c == '\\' || (c as u32) < 0x20);
54
55 if !needs_quoting {
56 return word.to_string();
57 }
58
59 if has_single_quote {
64 let mut out = String::new();
65 let mut in_squote = false;
66 for c in word.chars() {
67 if c == '\'' {
68 if in_squote {
69 out.push('\''); in_squote = false;
71 }
72 out.push_str("\\'");
73 } else {
74 if !in_squote {
75 out.push('\''); in_squote = true;
77 }
78 out.push(c);
79 }
80 }
81 if in_squote {
82 out.push('\'');
83 }
84 out
85 } else {
86 format!("'{word}'")
89 }
90}
91
92fn format_xtrace_command(ps4: &str, cmd: &str, args: &[String]) -> String {
94 let mut parts = Vec::with_capacity(1 + args.len());
95 parts.push(xtrace_quote(cmd));
96 for a in args {
97 parts.push(xtrace_quote(a));
98 }
99 format!("{ps4}{}\n", parts.join(" "))
100}
101
102fn check_errexit(state: &mut InterpreterState) {
107 if state.shell_opts.errexit
108 && state.last_exit_code != 0
109 && state.errexit_suppressed == 0
110 && !state.in_trap
111 {
112 state.should_exit = true;
113 }
114}
115
116fn check_limits(state: &InterpreterState) -> Result<(), RustBashError> {
118 if state.counters.command_count > state.limits.max_command_count {
119 return Err(RustBashError::LimitExceeded {
120 limit_name: "max_command_count",
121 limit_value: state.limits.max_command_count,
122 actual_value: state.counters.command_count,
123 });
124 }
125 if state.counters.output_size > state.limits.max_output_size {
126 return Err(RustBashError::LimitExceeded {
127 limit_name: "max_output_size",
128 limit_value: state.limits.max_output_size,
129 actual_value: state.counters.output_size,
130 });
131 }
132 if state.counters.start_time.elapsed() > state.limits.max_execution_time {
133 return Err(RustBashError::Timeout);
134 }
135 Ok(())
136}
137
138pub fn execute_program(
140 program: &ast::Program,
141 state: &mut InterpreterState,
142) -> Result<ExecResult, RustBashError> {
143 let mut result = ExecResult::default();
144
145 for complete_command in &program.complete_commands {
146 if state.should_exit {
147 break;
148 }
149 let r = execute_compound_list(complete_command, state, "")?;
150 state.counters.output_size += r.stdout.len() + r.stderr.len();
151 check_limits(state)?;
152 result.stdout.push_str(&r.stdout);
153 result.stderr.push_str(&r.stderr);
154 result.exit_code = r.exit_code;
155 state.last_exit_code = r.exit_code;
156 }
157
158 Ok(result)
159}
160
161fn execute_compound_list(
162 list: &ast::CompoundList,
163 state: &mut InterpreterState,
164 stdin: &str,
165) -> Result<ExecResult, RustBashError> {
166 let mut result = ExecResult::default();
167
168 for item in &list.0 {
169 if state.should_exit || state.control_flow.is_some() {
170 break;
171 }
172 let ast::CompoundListItem(and_or_list, _separator) = item;
173 let r = match execute_and_or_list(and_or_list, state, stdin) {
174 Ok(r) => r,
175 Err(RustBashError::Execution(msg)) if msg.contains("unbound variable") => {
176 state.should_exit = true;
178 state.last_exit_code = 1;
179 ExecResult {
180 stderr: format!("rust-bash: {msg}\n"),
181 exit_code: 1,
182 ..Default::default()
183 }
184 }
185 Err(e) => return Err(e),
186 };
187 result.stdout.push_str(&r.stdout);
188 result.stderr.push_str(&r.stderr);
189 result.exit_code = r.exit_code;
190 state.last_exit_code = r.exit_code;
191
192 if r.exit_code != 0
195 && !state.in_trap
196 && state.errexit_suppressed == 0
197 && let Some(err_cmd) = state.traps.get("ERR").cloned()
198 && !err_cmd.is_empty()
199 {
200 let trap_r = execute_trap(&err_cmd, state)?;
201 result.stdout.push_str(&trap_r.stdout);
202 result.stderr.push_str(&trap_r.stderr);
203 }
204 }
205
206 Ok(result)
207}
208
209fn execute_and_or_list(
210 aol: &ast::AndOrList,
211 state: &mut InterpreterState,
212 stdin: &str,
213) -> Result<ExecResult, RustBashError> {
214 let has_chain = !aol.additional.is_empty();
217 if has_chain {
218 state.errexit_suppressed += 1;
219 }
220 let mut result = execute_pipeline(&aol.first, state, stdin)?;
221 if has_chain {
222 state.errexit_suppressed -= 1;
223 }
224 state.last_exit_code = result.exit_code;
225
226 if !has_chain {
228 check_errexit(state);
229 if state.should_exit {
230 return Ok(result);
231 }
232 }
233
234 for (idx, and_or) in aol.additional.iter().enumerate() {
235 if state.should_exit || state.control_flow.is_some() {
236 break;
237 }
238 let (should_run, pipeline) = match and_or {
239 ast::AndOr::And(p) => (result.exit_code == 0, p),
240 ast::AndOr::Or(p) => (result.exit_code != 0, p),
241 };
242 if should_run {
243 let is_last = idx == aol.additional.len() - 1;
245 if !is_last {
246 state.errexit_suppressed += 1;
247 }
248 let r = execute_pipeline(pipeline, state, stdin)?;
249 if !is_last {
250 state.errexit_suppressed -= 1;
251 }
252 result.stdout.push_str(&r.stdout);
253 result.stderr.push_str(&r.stderr);
254 result.exit_code = r.exit_code;
255 state.last_exit_code = r.exit_code;
256
257 if is_last {
258 check_errexit(state);
259 }
260 }
261 }
262
263 Ok(result)
264}
265
266fn execute_pipeline(
267 pipeline: &ast::Pipeline,
268 state: &mut InterpreterState,
269 stdin: &str,
270) -> Result<ExecResult, RustBashError> {
271 let timed = pipeline.timed.is_some();
272 let start = if timed {
273 Some(crate::platform::Instant::now())
274 } else {
275 None
276 };
277
278 let mut pipe_data = stdin.to_string();
279 let mut pipe_data_bytes: Option<Vec<u8>> = None;
280 let mut combined_stderr = String::new();
281 let mut exit_code = 0;
282 let mut exit_codes: Vec<i32> = Vec::new();
283 let is_actual_pipe = pipeline.seq.len() > 1;
284 let saved_stdin_offset = state.stdin_offset;
285
286 if pipeline.bang {
288 state.errexit_suppressed += 1;
289 }
290
291 for (idx, command) in pipeline.seq.iter().enumerate() {
292 if state.should_exit || state.control_flow.is_some() {
293 break;
294 }
295 if idx > 0 {
297 state.stdin_offset = 0;
298 }
299 state.pipe_stdin_bytes = pipe_data_bytes.take();
301 let r = execute_command(command, state, &pipe_data)?;
302 if let Some(bytes) = r.stdout_bytes {
304 pipe_data_bytes = Some(bytes);
305 pipe_data = String::new();
306 } else {
307 pipe_data = r.stdout;
308 pipe_data_bytes = None;
309 }
310 combined_stderr.push_str(&r.stderr);
311 exit_code = r.exit_code;
312 exit_codes.push(r.exit_code);
313 }
314 state.pipe_stdin_bytes = None;
316
317 if is_actual_pipe {
321 state.stdin_offset = saved_stdin_offset;
322 }
323
324 if pipeline.bang {
325 state.errexit_suppressed -= 1;
326 }
327
328 if state.shell_opts.pipefail {
330 exit_code = exit_codes
331 .iter()
332 .rev()
333 .copied()
334 .find(|&c| c != 0)
335 .unwrap_or(0);
336 }
337
338 let exit_code = if pipeline.bang {
339 i32::from(exit_code == 0)
340 } else {
341 exit_code
342 };
343
344 let mut pipestatus_map = std::collections::BTreeMap::new();
347 for (i, code) in exit_codes.iter().enumerate() {
348 pipestatus_map.insert(i, code.to_string());
349 }
350 state.env.insert(
351 "PIPESTATUS".to_string(),
352 Variable {
353 value: VariableValue::IndexedArray(pipestatus_map),
354 attrs: VariableAttrs::empty(),
355 },
356 );
357
358 if let Some(start) = start {
360 let elapsed = start.elapsed();
361 let total_secs = elapsed.as_secs_f64();
362 let mins = total_secs as u64 / 60;
363 let secs = total_secs - (mins as f64 * 60.0);
364 combined_stderr.push_str(&format!(
365 "\nreal\t{}m{:.3}s\nuser\t0m0.000s\nsys\t0m0.000s\n",
366 mins, secs
367 ));
368 }
369
370 let final_stdout = if let Some(bytes) = pipe_data_bytes {
372 String::from_utf8_lossy(&bytes).into_owned()
373 } else {
374 pipe_data
375 };
376
377 Ok(ExecResult {
378 stdout: final_stdout,
379 stderr: combined_stderr,
380 exit_code,
381 stdout_bytes: None,
382 })
383}
384
385fn execute_command(
386 command: &ast::Command,
387 state: &mut InterpreterState,
388 stdin: &str,
389) -> Result<ExecResult, RustBashError> {
390 if let Some(loc) = command.location() {
392 state.current_lineno = loc.start.line;
393 }
394
395 if state.shell_opts.noexec && !matches!(command, ast::Command::Simple(_)) {
397 return Ok(ExecResult::default());
398 }
399
400 let result = match command {
401 ast::Command::Simple(simple_cmd) => execute_simple_command(simple_cmd, state, stdin),
402 ast::Command::Compound(compound, redirects) => {
403 execute_compound_command(compound, redirects.as_ref(), state, stdin)
404 }
405 ast::Command::Function(func_def) => {
406 match expand_word_to_string_mut(&func_def.fname, state) {
407 Ok(name) => {
408 state.functions.insert(
409 name,
410 FunctionDef {
411 body: func_def.body.clone(),
412 },
413 );
414 Ok(ExecResult::default())
415 }
416 Err(e) => Err(e),
417 }
418 }
419 ast::Command::ExtendedTest(ext_test) => execute_extended_test(&ext_test.expr, state),
420 };
421
422 match result {
423 Err(RustBashError::ExpansionError {
424 message,
425 exit_code,
426 should_exit,
427 }) => {
428 state.last_exit_code = exit_code;
429 if should_exit {
430 state.should_exit = true;
431 }
432 Ok(ExecResult {
433 stderr: format!("rust-bash: {message}\n"),
434 exit_code,
435 ..Default::default()
436 })
437 }
438 Err(RustBashError::FailGlob { pattern }) => {
439 state.last_exit_code = 1;
440 Ok(ExecResult {
441 stderr: format!("rust-bash: no match: {pattern}\n"),
442 exit_code: 1,
443 ..Default::default()
444 })
445 }
446 other => other,
447 }
448}
449
450#[derive(Debug, Clone)]
454enum Assignment {
455 Scalar { name: String, value: String },
457 IndexedArray {
459 name: String,
460 elements: Vec<(Option<usize>, String)>,
461 },
462 AssocArray {
464 name: String,
465 elements: Vec<(String, String)>,
466 },
467 ArrayElement {
469 name: String,
470 index: String,
471 value: String,
472 },
473 AppendArrayElement {
475 name: String,
476 index: String,
477 value: String,
478 },
479 AppendArray {
481 name: String,
482 elements: Vec<(Option<usize>, String)>,
483 },
484 AppendAssocArray {
486 name: String,
487 elements: Vec<(String, String)>,
488 },
489 AppendScalar { name: String, value: String },
491}
492
493impl Assignment {
494 fn name(&self) -> &str {
495 match self {
496 Assignment::Scalar { name, .. }
497 | Assignment::IndexedArray { name, .. }
498 | Assignment::AssocArray { name, .. }
499 | Assignment::ArrayElement { name, .. }
500 | Assignment::AppendArrayElement { name, .. }
501 | Assignment::AppendArray { name, .. }
502 | Assignment::AppendAssocArray { name, .. }
503 | Assignment::AppendScalar { name, .. } => name,
504 }
505 }
506}
507
508fn process_assignment(
510 assignment: &ast::Assignment,
511 append: bool,
512 state: &mut InterpreterState,
513) -> Result<Assignment, RustBashError> {
514 match (&assignment.name, &assignment.value) {
515 (ast::AssignmentName::VariableName(name), ast::AssignmentValue::Scalar(w)) => {
516 let value = expand_word_to_string_mut(w, state)?;
517 if append {
518 Ok(Assignment::AppendScalar {
519 name: name.clone(),
520 value,
521 })
522 } else {
523 Ok(Assignment::Scalar {
524 name: name.clone(),
525 value,
526 })
527 }
528 }
529 (ast::AssignmentName::VariableName(name), ast::AssignmentValue::Array(items)) => {
530 let is_assoc = state
532 .env
533 .get(name)
534 .is_some_and(|v| matches!(v.value, VariableValue::AssociativeArray(_)));
535 if is_assoc {
536 let mut elements = Vec::new();
537 for (opt_idx_word, val_word) in items {
538 let key = if let Some(idx_word) = opt_idx_word {
539 expand_word_to_string_mut(idx_word, state)?
540 } else {
541 String::new()
543 };
544 let val = expand_word_to_string_mut(val_word, state)?;
545 elements.push((key, val));
546 }
547 if append {
548 Ok(Assignment::AppendAssocArray {
549 name: name.clone(),
550 elements,
551 })
552 } else {
553 Ok(Assignment::AssocArray {
554 name: name.clone(),
555 elements,
556 })
557 }
558 } else {
559 let mut elements = Vec::new();
560 for (opt_idx_word, val_word) in items {
561 let idx = if let Some(idx_word) = opt_idx_word {
562 let idx_str = expand_word_to_string_mut(idx_word, state)?;
563 let idx_val =
564 crate::interpreter::arithmetic::eval_arithmetic(&idx_str, state)?;
565 if idx_val < 0 {
566 return Err(RustBashError::Execution(format!(
567 "negative array subscript: {idx_val}"
568 )));
569 }
570 Some(idx_val as usize)
571 } else {
572 None
573 };
574 let vals = expand_word_mut(val_word, state)?;
577 if vals.is_empty() {
578 elements.push((idx, String::new()));
579 } else {
580 for (i, val) in vals.into_iter().enumerate() {
581 if i == 0 {
582 elements.push((idx, val));
583 } else {
584 elements.push((None, val));
586 }
587 }
588 }
589 }
590 if append {
591 Ok(Assignment::AppendArray {
592 name: name.clone(),
593 elements,
594 })
595 } else {
596 Ok(Assignment::IndexedArray {
597 name: name.clone(),
598 elements,
599 })
600 }
601 }
602 }
603 (
604 ast::AssignmentName::ArrayElementName(name, index_str),
605 ast::AssignmentValue::Scalar(w),
606 ) => {
607 let value = expand_word_to_string_mut(w, state)?;
608 let index_word = ast::Word {
610 value: index_str.clone(),
611 loc: None,
612 };
613 let expanded_index = expand_word_to_string_mut(&index_word, state)?;
614 if append {
615 Ok(Assignment::AppendArrayElement {
616 name: name.clone(),
617 index: expanded_index,
618 value,
619 })
620 } else {
621 Ok(Assignment::ArrayElement {
622 name: name.clone(),
623 index: expanded_index,
624 value,
625 })
626 }
627 }
628 (ast::AssignmentName::ArrayElementName(name, _), ast::AssignmentValue::Array(_)) => Err(
629 RustBashError::Execution(format!("{name}: cannot assign array to array element")),
630 ),
631 }
632}
633
634fn apply_assignment(
636 assignment: Assignment,
637 state: &mut InterpreterState,
638) -> Result<(), RustBashError> {
639 match assignment {
640 Assignment::Scalar { name, value } => {
641 set_variable(state, &name, value)?;
642 }
643 Assignment::IndexedArray { name, elements } => {
644 if let Some(var) = state.env.get(&name)
645 && var.readonly()
646 {
647 return Err(RustBashError::Execution(format!(
648 "{name}: readonly variable"
649 )));
650 }
651 let limit = state.limits.max_array_elements;
652 let mut map = std::collections::BTreeMap::new();
653 let mut auto_idx: usize = 0;
654 for (opt_idx, val) in elements {
655 let idx = opt_idx.unwrap_or(auto_idx);
656 if map.len() >= limit {
657 return Err(RustBashError::LimitExceeded {
658 limit_name: "max_array_elements",
659 limit_value: limit,
660 actual_value: map.len() + 1,
661 });
662 }
663 map.insert(idx, val);
664 auto_idx = idx + 1;
665 }
666 let attrs = state
667 .env
668 .get(&name)
669 .map(|v| v.attrs)
670 .unwrap_or(VariableAttrs::empty());
671 state.env.insert(
672 name,
673 Variable {
674 value: VariableValue::IndexedArray(map),
675 attrs,
676 },
677 );
678 }
679 Assignment::AssocArray { name, elements } => {
680 if let Some(var) = state.env.get(&name)
681 && var.readonly()
682 {
683 return Err(RustBashError::Execution(format!(
684 "{name}: readonly variable"
685 )));
686 }
687 let limit = state.limits.max_array_elements;
688 let mut map = std::collections::BTreeMap::new();
689 for (key, val) in elements {
690 if map.len() >= limit {
691 return Err(RustBashError::LimitExceeded {
692 limit_name: "max_array_elements",
693 limit_value: limit,
694 actual_value: map.len() + 1,
695 });
696 }
697 map.insert(key, val);
698 }
699 let attrs = state
700 .env
701 .get(&name)
702 .map(|v| v.attrs)
703 .unwrap_or(VariableAttrs::empty());
704 state.env.insert(
705 name,
706 Variable {
707 value: VariableValue::AssociativeArray(map),
708 attrs,
709 },
710 );
711 }
712 Assignment::ArrayElement { name, index, value } => {
713 let is_assoc = state
715 .env
716 .get(&name)
717 .is_some_and(|v| matches!(v.value, VariableValue::AssociativeArray(_)));
718 if is_assoc {
719 crate::interpreter::set_assoc_element(state, &name, index, value)?;
720 } else {
721 let idx = crate::interpreter::arithmetic::eval_arithmetic(&index, state)?;
723 let uidx = resolve_negative_array_index(idx, &name, state)?;
724 set_array_element(state, &name, uidx, value)?;
725 }
726 }
727 Assignment::AppendArrayElement { name, index, value } => {
728 let is_assoc = state
729 .env
730 .get(&name)
731 .is_some_and(|v| matches!(v.value, VariableValue::AssociativeArray(_)));
732 if is_assoc {
733 let current = state
734 .env
735 .get(&name)
736 .and_then(|v| match &v.value {
737 VariableValue::AssociativeArray(map) => map.get(&index).cloned(),
738 _ => None,
739 })
740 .unwrap_or_default();
741 let new_val = format!("{current}{value}");
742 crate::interpreter::set_assoc_element(state, &name, index, new_val)?;
743 } else {
744 let idx = crate::interpreter::arithmetic::eval_arithmetic(&index, state)?;
745 let uidx = resolve_negative_array_index(idx, &name, state)?;
746 let current = state
747 .env
748 .get(&name)
749 .and_then(|v| match &v.value {
750 VariableValue::IndexedArray(map) => map.get(&uidx).cloned(),
751 VariableValue::Scalar(s) if uidx == 0 => Some(s.clone()),
752 _ => None,
753 })
754 .unwrap_or_default();
755 let new_val = format!("{current}{value}");
756 set_array_element(state, &name, uidx, new_val)?;
757 }
758 }
759 Assignment::AppendArray { name, elements } => {
760 let start_idx = match state.env.get(&name) {
762 Some(var) => match &var.value {
763 VariableValue::IndexedArray(map) => {
764 map.keys().next_back().map(|k| k + 1).unwrap_or(0)
765 }
766 VariableValue::Scalar(s) if s.is_empty() => 0,
767 VariableValue::Scalar(_) => 1,
768 VariableValue::AssociativeArray(_) => 0,
769 },
770 None => 0,
771 };
772
773 if !state.env.contains_key(&name) {
775 state.env.insert(
776 name.clone(),
777 Variable {
778 value: VariableValue::IndexedArray(std::collections::BTreeMap::new()),
779 attrs: VariableAttrs::empty(),
780 },
781 );
782 }
783
784 if let Some(var) = state.env.get_mut(&name)
786 && let VariableValue::Scalar(s) = &var.value
787 {
788 let mut map = std::collections::BTreeMap::new();
789 if !s.is_empty() {
790 map.insert(0, s.clone());
791 }
792 var.value = VariableValue::IndexedArray(map);
793 }
794
795 let mut auto_idx = start_idx;
796 for (opt_idx, val) in elements {
797 let idx = opt_idx.unwrap_or(auto_idx);
798 set_array_element(state, &name, idx, val)?;
799 auto_idx = idx + 1;
800 }
801 }
802 Assignment::AppendAssocArray { name, elements } => {
803 if !state.env.contains_key(&name) {
805 state.env.insert(
806 name.clone(),
807 Variable {
808 value: VariableValue::AssociativeArray(std::collections::BTreeMap::new()),
809 attrs: VariableAttrs::empty(),
810 },
811 );
812 }
813 for (key, val) in elements {
814 crate::interpreter::set_assoc_element(state, &name, key, val)?;
815 }
816 }
817 Assignment::AppendScalar { name, value } => {
818 let target = crate::interpreter::resolve_nameref(&name, state)?;
820 let is_integer = state
821 .env
822 .get(&target)
823 .is_some_and(|v| v.attrs.contains(VariableAttrs::INTEGER));
824 if is_integer {
825 let current = state
826 .env
827 .get(&target)
828 .map(|v| v.value.as_scalar().to_string())
829 .unwrap_or_else(|| "0".to_string());
830 let expr = format!("{current}+{value}");
831 set_variable(state, &name, expr)?;
832 } else {
833 match state.env.get(&target) {
834 Some(var) => {
835 let new_val = format!("{}{}", var.value.as_scalar(), value);
836 set_variable(state, &name, new_val)?;
837 }
838 None => {
839 set_variable(state, &name, value)?;
840 }
841 }
842 }
843 }
844 }
845 Ok(())
846}
847
848fn resolve_negative_array_index(
851 idx: i64,
852 name: &str,
853 state: &InterpreterState,
854) -> Result<usize, RustBashError> {
855 if idx >= 0 {
856 return Ok(idx as usize);
857 }
858 let max_key = state.env.get(name).and_then(|v| match &v.value {
859 VariableValue::IndexedArray(map) => map.keys().next_back().copied(),
860 VariableValue::Scalar(_) => Some(0),
861 _ => None,
862 });
863 match max_key {
864 Some(mk) => {
865 let resolved = mk as i64 + 1 + idx;
866 if resolved < 0 {
867 Err(RustBashError::Execution(format!(
868 "{name}: bad array subscript"
869 )))
870 } else {
871 Ok(resolved as usize)
872 }
873 }
874 None => Err(RustBashError::Execution(format!(
875 "{name}: bad array subscript"
876 ))),
877 }
878}
879
880fn apply_assignment_shell_error(
885 assignment: Assignment,
886 state: &mut InterpreterState,
887 result: &mut ExecResult,
888) -> Result<(), RustBashError> {
889 match apply_assignment(assignment, state) {
890 Ok(()) => Ok(()),
891 Err(RustBashError::Execution(msg)) => {
892 result.stderr.push_str(&format!("rust-bash: {msg}\n"));
893 result.exit_code = 1;
894 state.last_exit_code = 1;
895 Ok(())
896 }
897 Err(other) => Err(other),
898 }
899}
900
901fn execute_simple_command(
902 cmd: &ast::SimpleCommand,
903 state: &mut InterpreterState,
904 stdin: &str,
905) -> Result<ExecResult, RustBashError> {
906 if state.shell_opts.noexec {
908 return Ok(ExecResult::default());
909 }
910
911 let mut assignments: Vec<Assignment> = Vec::new();
913 let mut redirects: Vec<&ast::IoRedirect> = Vec::new();
914 let mut proc_sub_temps: Vec<String> = Vec::new();
916 let mut deferred_write_subs: Vec<(&ast::CompoundList, String)> = Vec::new();
918
919 if let Some(prefix) = &cmd.prefix {
920 for item in &prefix.0 {
921 match item {
922 ast::CommandPrefixOrSuffixItem::AssignmentWord(assignment, _word) => {
923 let a = process_assignment(assignment, assignment.append, state)?;
924 assignments.push(a);
925 }
926 ast::CommandPrefixOrSuffixItem::IoRedirect(redir) => {
927 redirects.push(redir);
928 }
929 ast::CommandPrefixOrSuffixItem::ProcessSubstitution(kind, subshell) => {
930 let path = expand_process_substitution(
931 kind,
932 &subshell.list,
933 state,
934 &mut deferred_write_subs,
935 )?;
936 proc_sub_temps.push(path);
937 }
938 _ => {}
939 }
940 }
941 }
942
943 let cmd_name = cmd
945 .word_or_name
946 .as_ref()
947 .map(|w| expand_word_to_string_mut(w, state))
948 .transpose()?;
949
950 let mut args: Vec<String> = Vec::new();
952 if let Some(suffix) = &cmd.suffix {
953 for item in &suffix.0 {
954 match item {
955 ast::CommandPrefixOrSuffixItem::Word(w) => match expand_word_mut(w, state) {
956 Ok(expanded) => args.extend(expanded),
957 Err(RustBashError::FailGlob { pattern }) => {
958 state.last_exit_code = 1;
959 return Ok(ExecResult {
960 stderr: format!("rust-bash: no match: {pattern}\n"),
961 exit_code: 1,
962 ..Default::default()
963 });
964 }
965 Err(e) => return Err(e),
966 },
967 ast::CommandPrefixOrSuffixItem::IoRedirect(redir) => {
968 redirects.push(redir);
969 }
970 ast::CommandPrefixOrSuffixItem::AssignmentWord(assignment, _word) => {
971 let name = match &assignment.name {
972 ast::AssignmentName::VariableName(n) => n.clone(),
973 ast::AssignmentName::ArrayElementName(n, idx) => {
974 let idx_word = ast::Word {
975 value: idx.clone(),
976 loc: None,
977 };
978 let expanded_idx = expand_word_to_string_mut(&idx_word, state)?;
979 format!("{n}[{expanded_idx}]")
980 }
981 };
982 match &assignment.value {
983 ast::AssignmentValue::Scalar(w) => {
984 let value = expand_word_to_string_mut(w, state)?;
985 args.push(format!("{name}={value}"));
986 }
987 ast::AssignmentValue::Array(items) => {
988 let mut parts = Vec::new();
989 for (opt_idx_word, val_word) in items {
990 let vals = expand_word_mut(val_word, state)?;
991 if let Some(idx_word) = opt_idx_word {
992 let idx_str = expand_word_to_string_mut(idx_word, state)?;
993 let first = vals.first().cloned().unwrap_or_default();
994 parts.push(format!("[{idx_str}]={first}"));
995 for v in vals.into_iter().skip(1) {
996 parts.push(v);
997 }
998 } else {
999 parts.extend(vals);
1000 }
1001 }
1002 args.push(format!("{name}=({})", parts.join(" ")));
1003 }
1004 }
1005 }
1006 ast::CommandPrefixOrSuffixItem::ProcessSubstitution(kind, subshell) => {
1007 let path = expand_process_substitution(
1008 kind,
1009 &subshell.list,
1010 state,
1011 &mut deferred_write_subs,
1012 )?;
1013 proc_sub_temps.push(path.clone());
1014 args.push(path);
1015 }
1016 }
1017 }
1018 }
1019
1020 let Some(cmd_name) = cmd_name else {
1022 if state.shell_opts.xtrace && !assignments.is_empty() {
1024 let ps4 = expand_ps4(state);
1025 let mut trace = String::new();
1026 for a in &assignments {
1027 let part = match a {
1028 Assignment::Scalar { name, value } => format!("{name}={value}"),
1029 Assignment::IndexedArray { name, elements, .. } => {
1030 let vals: Vec<String> =
1031 elements.iter().map(|(_, v)| xtrace_quote(v)).collect();
1032 format!("{name}=({})", vals.join(" "))
1033 }
1034 Assignment::ArrayElement {
1035 name, index, value, ..
1036 } => format!("{name}[{index}]={value}"),
1037 Assignment::AppendArrayElement {
1038 name, index, value, ..
1039 } => format!("{name}[{index}]+={value}"),
1040 Assignment::AppendArray { name, elements, .. } => {
1041 let vals: Vec<String> =
1042 elements.iter().map(|(_, v)| xtrace_quote(v)).collect();
1043 format!("{name}+=({})", vals.join(" "))
1044 }
1045 Assignment::AssocArray { name, .. } => format!("{name}=(...)"),
1046 Assignment::AppendAssocArray { name, .. } => format!("{name}+=(...)"),
1047 Assignment::AppendScalar { name, value } => format!("{name}+={value}"),
1048 };
1049 trace.push_str(&format!("{ps4}{part}\n"));
1050 }
1051 let mut result = ExecResult {
1053 stderr: trace,
1054 ..ExecResult::default()
1055 };
1056 for a in assignments {
1057 apply_assignment_shell_error(a, state, &mut result)?;
1058 }
1059 if !state.pending_cmdsub_stderr.is_empty() {
1060 let cmdsub_stderr = std::mem::take(&mut state.pending_cmdsub_stderr);
1061 result.stderr = format!("{cmdsub_stderr}{}", result.stderr);
1062 }
1063 return Ok(result);
1064 }
1065 let mut result = ExecResult::default();
1066 for a in assignments {
1067 apply_assignment_shell_error(a, state, &mut result)?;
1068 }
1069 if !state.pending_cmdsub_stderr.is_empty() {
1070 let cmdsub_stderr = std::mem::take(&mut state.pending_cmdsub_stderr);
1071 result.stderr = format!("{cmdsub_stderr}{}", result.stderr);
1072 }
1073 return Ok(result);
1074 };
1075
1076 if cmd_name.is_empty() && args.is_empty() {
1078 let mut result = ExecResult {
1079 exit_code: state.last_exit_code,
1080 ..ExecResult::default()
1081 };
1082 for a in assignments {
1083 apply_assignment_shell_error(a, state, &mut result)?;
1084 }
1085 if !state.pending_cmdsub_stderr.is_empty() {
1086 let cmdsub_stderr = std::mem::take(&mut state.pending_cmdsub_stderr);
1087 result.stderr = format!("{cmdsub_stderr}{}", result.stderr);
1088 }
1089 return Ok(result);
1090 }
1091
1092 let (cmd_name, args) = if state.shopt_opts.expand_aliases {
1095 if let Some(expansion) = state.aliases.get(&cmd_name).cloned() {
1096 let mut parts: Vec<String> = expansion
1097 .split_whitespace()
1098 .map(|s| s.to_string())
1099 .collect();
1100 if parts.is_empty() {
1101 (cmd_name, args)
1102 } else {
1103 let new_cmd = parts.remove(0);
1104 parts.extend(args);
1105 (new_cmd, parts)
1106 }
1107 } else {
1108 (cmd_name, args)
1109 }
1110 } else {
1111 (cmd_name, args)
1112 };
1113
1114 if cmd_name == "exec" {
1117 if args.first().map(|a| a.as_str()) == Some("--help")
1119 && let Some(meta) = builtins::builtin_meta("exec")
1120 && meta.supports_help_flag
1121 {
1122 return Ok(ExecResult {
1123 stdout: crate::commands::format_help(meta),
1124 stderr: String::new(),
1125 exit_code: 0,
1126 stdout_bytes: None,
1127 });
1128 }
1129 for a in &assignments {
1130 let mut dummy = ExecResult::default();
1131 apply_assignment_shell_error(a.clone(), state, &mut dummy)?;
1132 if dummy.exit_code != 0 {
1133 return Ok(dummy);
1134 }
1135 }
1136 return execute_exec_builtin(&args, &redirects, state, stdin);
1137 }
1138
1139 let mut saved: Vec<(String, Option<Variable>)> = Vec::new();
1144 let mut prefix_stderr = String::new();
1145 for a in &assignments {
1146 saved.push((a.name().to_string(), state.env.get(a.name()).cloned()));
1147 let mut dummy = ExecResult::default();
1148 apply_assignment_shell_error(a.clone(), state, &mut dummy)?;
1149 if dummy.exit_code != 0 {
1150 prefix_stderr.push_str(&dummy.stderr);
1151 }
1152 if let Some(var) = state.env.get_mut(a.name()) {
1155 var.attrs.insert(VariableAttrs::EXPORTED);
1156 }
1157 }
1158
1159 struct RedirProcSub<'a> {
1162 temp_path: String,
1163 kind: &'a ast::ProcessSubstitutionKind,
1164 list: &'a ast::CompoundList,
1165 }
1166 let last_arg = args.last().cloned().unwrap_or_else(|| cmd_name.clone());
1167 let should_trace = state.shell_opts.xtrace;
1168 let pre_ps4 = if should_trace {
1170 Some(expand_ps4(state))
1171 } else {
1172 None
1173 };
1174
1175 let mut inner_result = (|| -> Result<ExecResult, RustBashError> {
1176 let mut redir_proc_subs: Vec<RedirProcSub<'_>> = Vec::new();
1181 for redir in &redirects {
1182 if let ast::IoRedirect::File(
1183 _,
1184 _,
1185 target @ ast::IoFileRedirectTarget::ProcessSubstitution(kind, subshell),
1186 ) = redir
1187 {
1188 let temp_path = match kind {
1189 ast::ProcessSubstitutionKind::Read => {
1190 execute_read_process_substitution(&subshell.list, state)?
1191 }
1192 ast::ProcessSubstitutionKind::Write => allocate_proc_sub_temp_file(state, b"")?,
1193 };
1194 proc_sub_temps.push(temp_path.clone());
1195 let key = std::ptr::from_ref(target) as usize;
1198 state.proc_sub_prealloc.insert(key, temp_path.clone());
1199 redir_proc_subs.push(RedirProcSub {
1200 temp_path,
1201 kind,
1202 list: &subshell.list,
1203 });
1204 }
1205 }
1206
1207 let effective_stdin = match get_stdin_from_redirects(&redirects, state, stdin) {
1209 Ok(s) => s,
1210 Err(RustBashError::RedirectFailed(msg)) => {
1211 let mut result = ExecResult {
1212 stderr: format!("rust-bash: {msg}\n"),
1213 exit_code: 1,
1214 ..ExecResult::default()
1215 };
1216 state.last_exit_code = 1;
1217 apply_output_redirects(&redirects, &mut result, state)?;
1218 return Ok(result);
1219 }
1220 Err(e) => return Err(e),
1221 };
1222
1223 state.last_argument = last_arg.clone();
1225
1226 let mut result = dispatch_command(&cmd_name, &args, state, &effective_stdin)?;
1231
1232 if !state.pending_cmdsub_stderr.is_empty() {
1234 let cmdsub_stderr = std::mem::take(&mut state.pending_cmdsub_stderr);
1235 result.stderr = format!("{cmdsub_stderr}{}", result.stderr);
1236 }
1237
1238 if let Some(ref ps4) = pre_ps4 {
1240 let mut trace = format_xtrace_command(ps4, &cmd_name, &args);
1241 if matches!(
1245 cmd_name.as_str(),
1246 "readonly" | "declare" | "typeset" | "export"
1247 ) {
1248 for arg in &args {
1249 if let Some(eq_pos) = arg.find('=') {
1250 let name_part = &arg[..eq_pos];
1251 if !name_part.is_empty()
1253 && !name_part.starts_with('-')
1254 && name_part
1255 .chars()
1256 .all(|c| c.is_alphanumeric() || c == '_' || c == '+')
1257 {
1258 trace.push_str(&format!("{ps4}{arg}\n"));
1259 }
1260 }
1261 }
1262 }
1263 result.stderr = format!("{trace}{}", result.stderr);
1265 }
1266
1267 apply_output_redirects(&redirects, &mut result, state)?;
1269
1270 for rps in &redir_proc_subs {
1272 if matches!(rps.kind, ast::ProcessSubstitutionKind::Write) {
1273 let content = state
1274 .fs
1275 .read_file(Path::new(&rps.temp_path))
1276 .map_err(|e| RustBashError::Execution(e.to_string()))?;
1277 let stdin_data = String::from_utf8_lossy(&content).to_string();
1278 let mut sub_state = make_proc_sub_state(state);
1279 let inner_result = execute_compound_list(rps.list, &mut sub_state, &stdin_data)?;
1280 state.counters.command_count = sub_state.counters.command_count;
1281 state.counters.output_size = sub_state.counters.output_size;
1282 state.proc_sub_counter = sub_state.proc_sub_counter;
1283 result.stdout.push_str(&inner_result.stdout);
1284 result.stderr.push_str(&inner_result.stderr);
1285 }
1286 }
1287
1288 for (inner_list, temp_path) in &deferred_write_subs {
1290 let content = state
1291 .fs
1292 .read_file(Path::new(temp_path))
1293 .map_err(|e| RustBashError::Execution(e.to_string()))?;
1294 let stdin_data = String::from_utf8_lossy(&content).to_string();
1295 let mut sub_state = make_proc_sub_state(state);
1296 let inner_result = execute_compound_list(inner_list, &mut sub_state, &stdin_data)?;
1297 state.counters.command_count = sub_state.counters.command_count;
1298 state.counters.output_size = sub_state.counters.output_size;
1299 state.proc_sub_counter = sub_state.proc_sub_counter;
1300 result.stdout.push_str(&inner_result.stdout);
1301 result.stderr.push_str(&inner_result.stderr);
1302 }
1303
1304 Ok(result)
1305 })();
1306
1307 for temp_path in &proc_sub_temps {
1309 let _ = state.fs.remove_file(Path::new(temp_path));
1310 }
1311 state.proc_sub_prealloc.clear();
1312
1313 for (name, old_value) in saved {
1315 match old_value {
1316 Some(var) => {
1317 state.env.insert(name, var);
1318 }
1319 None => {
1320 state.env.remove(&name);
1321 }
1322 }
1323 }
1324
1325 if let Ok(ref mut r) = inner_result
1327 && !prefix_stderr.is_empty()
1328 {
1329 r.stderr = format!("{prefix_stderr}{}", r.stderr);
1330 }
1331
1332 inner_result
1333}
1334
1335fn execute_compound_command(
1338 compound: &ast::CompoundCommand,
1339 redirects: Option<&ast::RedirectList>,
1340 state: &mut InterpreterState,
1341 stdin: &str,
1342) -> Result<ExecResult, RustBashError> {
1343 let mut result = match compound {
1344 ast::CompoundCommand::IfClause(if_clause) => execute_if(if_clause, state, stdin)?,
1345 ast::CompoundCommand::ForClause(for_clause) => execute_for(for_clause, state, stdin)?,
1346 ast::CompoundCommand::WhileClause(wc) => execute_while_until(wc, false, state, stdin)?,
1347 ast::CompoundCommand::UntilClause(uc) => execute_while_until(uc, true, state, stdin)?,
1348 ast::CompoundCommand::BraceGroup(bg) => execute_compound_list(&bg.list, state, stdin)?,
1349 ast::CompoundCommand::Subshell(sub) => execute_subshell(&sub.list, state, stdin)?,
1350 ast::CompoundCommand::CaseClause(cc) => execute_case(cc, state, stdin)?,
1351 ast::CompoundCommand::Arithmetic(arith) => execute_arithmetic(arith, state)?,
1352 ast::CompoundCommand::ArithmeticForClause(afc) => {
1353 execute_arithmetic_for(afc, state, stdin)?
1354 }
1355 };
1356
1357 if !state.pending_cmdsub_stderr.is_empty() {
1359 let cmdsub_stderr = std::mem::take(&mut state.pending_cmdsub_stderr);
1360 result.stderr = format!("{cmdsub_stderr}{}", result.stderr);
1361 }
1362
1363 if let Some(redir_list) = redirects {
1365 let redir_refs: Vec<&ast::IoRedirect> = redir_list.0.iter().collect();
1366 apply_output_redirects(&redir_refs, &mut result, state)?;
1367 }
1368
1369 state.last_exit_code = result.exit_code;
1370 Ok(result)
1371}
1372
1373fn execute_if(
1374 if_clause: &ast::IfClauseCommand,
1375 state: &mut InterpreterState,
1376 stdin: &str,
1377) -> Result<ExecResult, RustBashError> {
1378 let mut result = ExecResult::default();
1379
1380 state.errexit_suppressed += 1;
1382 let cond = execute_compound_list(&if_clause.condition, state, stdin)?;
1383 state.errexit_suppressed -= 1;
1384 result.stdout.push_str(&cond.stdout);
1385 result.stderr.push_str(&cond.stderr);
1386
1387 if cond.exit_code == 0 {
1388 let body = execute_compound_list(&if_clause.then, state, stdin)?;
1389 result.stdout.push_str(&body.stdout);
1390 result.stderr.push_str(&body.stderr);
1391 result.exit_code = body.exit_code;
1392 return Ok(result);
1393 }
1394
1395 if let Some(elses) = &if_clause.elses {
1397 for else_clause in elses {
1398 if let Some(condition) = &else_clause.condition {
1399 state.errexit_suppressed += 1;
1401 let cond = execute_compound_list(condition, state, stdin)?;
1402 state.errexit_suppressed -= 1;
1403 result.stdout.push_str(&cond.stdout);
1404 result.stderr.push_str(&cond.stderr);
1405 if cond.exit_code == 0 {
1406 let body = execute_compound_list(&else_clause.body, state, stdin)?;
1407 result.stdout.push_str(&body.stdout);
1408 result.stderr.push_str(&body.stderr);
1409 result.exit_code = body.exit_code;
1410 return Ok(result);
1411 }
1412 } else {
1413 let body = execute_compound_list(&else_clause.body, state, stdin)?;
1415 result.stdout.push_str(&body.stdout);
1416 result.stderr.push_str(&body.stderr);
1417 result.exit_code = body.exit_code;
1418 return Ok(result);
1419 }
1420 }
1421 }
1422
1423 result.exit_code = 0;
1425 Ok(result)
1426}
1427
1428fn execute_for(
1429 for_clause: &ast::ForClauseCommand,
1430 state: &mut InterpreterState,
1431 stdin: &str,
1432) -> Result<ExecResult, RustBashError> {
1433 use crate::interpreter::ControlFlow;
1434
1435 let mut result = ExecResult::default();
1436
1437 let var_name = &for_clause.variable_name;
1440 if !var_name.is_empty()
1441 && (!var_name
1442 .chars()
1443 .all(|c| c.is_ascii_alphanumeric() || c == '_')
1444 || var_name.starts_with(|c: char| c.is_ascii_digit()))
1445 {
1446 result.stderr = format!("rust-bash: `{var_name}': not a valid identifier\n");
1447 result.exit_code = 1;
1448 state.last_exit_code = 1;
1449 return Ok(result);
1450 }
1451
1452 let values: Vec<String> = if let Some(words) = &for_clause.values {
1453 let mut vals = Vec::new();
1454 for w in words {
1455 vals.extend(expand_word_mut(w, state)?);
1456 }
1457 vals
1458 } else {
1459 state.positional_params.clone()
1461 };
1462
1463 state.loop_depth += 1;
1464 let mut iterations: usize = 0;
1465 for val in &values {
1466 if state.should_exit {
1467 break;
1468 }
1469 iterations += 1;
1470 if iterations > state.limits.max_loop_iterations {
1471 state.loop_depth -= 1;
1472 return Err(RustBashError::LimitExceeded {
1473 limit_name: "max_loop_iterations",
1474 limit_value: state.limits.max_loop_iterations,
1475 actual_value: iterations,
1476 });
1477 }
1478
1479 set_variable(state, &for_clause.variable_name, val.clone())?;
1480 let r = execute_compound_list(&for_clause.body.list, state, stdin)?;
1481 result.stdout.push_str(&r.stdout);
1482 result.stderr.push_str(&r.stderr);
1483 result.exit_code = r.exit_code;
1484
1485 match state.control_flow.take() {
1486 Some(ControlFlow::Break(n)) => {
1487 if n > 1 {
1488 state.control_flow = Some(ControlFlow::Break(n - 1));
1489 }
1490 break;
1491 }
1492 Some(ControlFlow::Continue(n)) => {
1493 if n > 1 {
1494 state.control_flow = Some(ControlFlow::Continue(n - 1));
1495 break;
1496 }
1497 }
1499 Some(ret @ ControlFlow::Return(_)) => {
1500 state.control_flow = Some(ret);
1501 break;
1502 }
1503 None => {}
1504 }
1505 }
1506 state.loop_depth -= 1;
1507
1508 Ok(result)
1509}
1510
1511fn execute_arithmetic(
1512 arith: &ast::ArithmeticCommand,
1513 state: &mut InterpreterState,
1514) -> Result<ExecResult, RustBashError> {
1515 let expanded =
1516 crate::interpreter::expansion::expand_arith_expression(&arith.expr.value, state)?;
1517 let val = crate::interpreter::arithmetic::eval_arithmetic(&expanded, state)?;
1518 let mut result = ExecResult {
1519 exit_code: if val != 0 { 0 } else { 1 },
1520 ..Default::default()
1521 };
1522 if state.shell_opts.xtrace {
1523 let ps4 = expand_ps4(state);
1524 result.stderr = format!(
1525 "{ps4}(({}))\n{}",
1526 arith.expr.value.trim_end(),
1527 result.stderr
1528 );
1529 }
1530 Ok(result)
1531}
1532
1533fn execute_arithmetic_for(
1534 afc: &ast::ArithmeticForClauseCommand,
1535 state: &mut InterpreterState,
1536 stdin: &str,
1537) -> Result<ExecResult, RustBashError> {
1538 use crate::interpreter::ControlFlow;
1539
1540 if let Some(init) = &afc.initializer {
1542 let expanded = crate::interpreter::expansion::expand_arith_expression(&init.value, state)?;
1543 crate::interpreter::arithmetic::eval_arithmetic(&expanded, state)?;
1544 }
1545
1546 let mut result = ExecResult::default();
1547 let mut iterations: usize = 0;
1548
1549 state.loop_depth += 1;
1550 loop {
1551 if state.should_exit {
1552 break;
1553 }
1554 iterations += 1;
1555 if iterations > state.limits.max_loop_iterations {
1556 state.loop_depth -= 1;
1557 return Err(RustBashError::LimitExceeded {
1558 limit_name: "max_loop_iterations",
1559 limit_value: state.limits.max_loop_iterations,
1560 actual_value: iterations,
1561 });
1562 }
1563
1564 if let Some(cond) = &afc.condition {
1566 let expanded =
1567 crate::interpreter::expansion::expand_arith_expression(&cond.value, state)?;
1568 let val = crate::interpreter::arithmetic::eval_arithmetic(&expanded, state)?;
1569 if val == 0 {
1570 break;
1571 }
1572 }
1573
1574 let body = execute_compound_list(&afc.body.list, state, stdin)?;
1576 result.stdout.push_str(&body.stdout);
1577 result.stderr.push_str(&body.stderr);
1578 result.exit_code = body.exit_code;
1579
1580 match state.control_flow.take() {
1581 Some(ControlFlow::Break(n)) => {
1582 if n > 1 {
1583 state.control_flow = Some(ControlFlow::Break(n - 1));
1584 }
1585 break;
1586 }
1587 Some(ControlFlow::Continue(n)) => {
1588 if n > 1 {
1589 state.control_flow = Some(ControlFlow::Continue(n - 1));
1590 break;
1591 }
1592 }
1593 Some(ret @ ControlFlow::Return(_)) => {
1594 state.control_flow = Some(ret);
1595 break;
1596 }
1597 None => {}
1598 }
1599
1600 if let Some(upd) = &afc.updater {
1602 let expanded =
1603 crate::interpreter::expansion::expand_arith_expression(&upd.value, state)?;
1604 crate::interpreter::arithmetic::eval_arithmetic(&expanded, state)?;
1605 }
1606 }
1607 state.loop_depth -= 1;
1608
1609 Ok(result)
1610}
1611
1612fn execute_while_until(
1613 clause: &ast::WhileOrUntilClauseCommand,
1614 is_until: bool,
1615 state: &mut InterpreterState,
1616 stdin: &str,
1617) -> Result<ExecResult, RustBashError> {
1618 use crate::interpreter::ControlFlow;
1619
1620 let mut result = ExecResult::default();
1621 let mut iterations: usize = 0;
1622
1623 state.loop_depth += 1;
1624 loop {
1625 if state.should_exit {
1626 break;
1627 }
1628 iterations += 1;
1629 if iterations > state.limits.max_loop_iterations {
1630 state.loop_depth -= 1;
1631 return Err(RustBashError::LimitExceeded {
1632 limit_name: "max_loop_iterations",
1633 limit_value: state.limits.max_loop_iterations,
1634 actual_value: iterations,
1635 });
1636 }
1637
1638 state.errexit_suppressed += 1;
1640 let cond = execute_compound_list(&clause.0, state, stdin)?;
1641 state.errexit_suppressed -= 1;
1642 result.stdout.push_str(&cond.stdout);
1643 result.stderr.push_str(&cond.stderr);
1644
1645 let should_continue = if is_until {
1646 cond.exit_code != 0
1647 } else {
1648 cond.exit_code == 0
1649 };
1650
1651 if !should_continue {
1652 break;
1653 }
1654
1655 let body = execute_compound_list(&clause.1.list, state, stdin)?;
1656 result.stdout.push_str(&body.stdout);
1657 result.stderr.push_str(&body.stderr);
1658 result.exit_code = body.exit_code;
1659
1660 match state.control_flow.take() {
1661 Some(ControlFlow::Break(n)) => {
1662 if n > 1 {
1663 state.control_flow = Some(ControlFlow::Break(n - 1));
1664 }
1665 break;
1666 }
1667 Some(ControlFlow::Continue(n)) => {
1668 if n > 1 {
1669 state.control_flow = Some(ControlFlow::Continue(n - 1));
1670 break;
1671 }
1672 }
1674 Some(ret @ ControlFlow::Return(_)) => {
1675 state.control_flow = Some(ret);
1676 break;
1677 }
1678 None => {}
1679 }
1680 }
1681 state.loop_depth -= 1;
1682
1683 Ok(result)
1684}
1685
1686fn execute_subshell(
1687 list: &ast::CompoundList,
1688 state: &mut InterpreterState,
1689 stdin: &str,
1690) -> Result<ExecResult, RustBashError> {
1691 let cloned_fs = state.fs.deep_clone();
1693
1694 let mut sub_state = InterpreterState {
1695 fs: cloned_fs,
1696 env: state.env.clone(),
1697 cwd: state.cwd.clone(),
1698 functions: state.functions.clone(),
1699 last_exit_code: state.last_exit_code,
1700 commands: clone_commands(&state.commands),
1701 shell_opts: state.shell_opts.clone(),
1702 shopt_opts: state.shopt_opts.clone(),
1703 limits: state.limits.clone(),
1704 counters: ExecutionCounters {
1705 command_count: state.counters.command_count,
1706 output_size: state.counters.output_size,
1707 start_time: state.counters.start_time,
1708 substitution_depth: state.counters.substitution_depth,
1709 call_depth: 0,
1710 },
1711 network_policy: state.network_policy.clone(),
1712 should_exit: false,
1713 loop_depth: 0,
1714 control_flow: None,
1715 positional_params: state.positional_params.clone(),
1716 shell_name: state.shell_name.clone(),
1717 random_seed: state.random_seed,
1718 local_scopes: Vec::new(),
1719 in_function_depth: 0,
1720 traps: state.traps.clone(),
1721 in_trap: false,
1722 errexit_suppressed: 0,
1723 stdin_offset: 0,
1724 dir_stack: state.dir_stack.clone(),
1725 command_hash: state.command_hash.clone(),
1726 aliases: state.aliases.clone(),
1727 current_lineno: state.current_lineno,
1728 shell_start_time: state.shell_start_time,
1729 last_argument: state.last_argument.clone(),
1730 call_stack: state.call_stack.clone(),
1731 machtype: state.machtype.clone(),
1732 hosttype: state.hosttype.clone(),
1733 persistent_fds: state.persistent_fds.clone(),
1734 next_auto_fd: state.next_auto_fd,
1735 proc_sub_counter: state.proc_sub_counter,
1736 proc_sub_prealloc: HashMap::new(),
1737 pipe_stdin_bytes: None,
1738 pending_cmdsub_stderr: String::new(),
1739 };
1740
1741 let result = execute_compound_list(list, &mut sub_state, stdin);
1742
1743 state.counters.command_count = sub_state.counters.command_count;
1745 state.counters.output_size = sub_state.counters.output_size;
1746
1747 let result = result?;
1748
1749 Ok(result)
1751}
1752
1753fn execute_case(
1754 case_clause: &ast::CaseClauseCommand,
1755 state: &mut InterpreterState,
1756 stdin: &str,
1757) -> Result<ExecResult, RustBashError> {
1758 let value = expand_word_to_string_mut(&case_clause.value, state)?;
1759 let mut result = ExecResult::default();
1760
1761 let mut i = 0;
1762 let mut fall_through = false;
1763 while i < case_clause.cases.len() {
1764 let case_item = &case_clause.cases[i];
1765
1766 let matched = if fall_through {
1767 fall_through = false;
1768 true
1769 } else {
1770 let mut m = false;
1771 for pattern_word in &case_item.patterns {
1772 let pattern = expand_word_to_string_mut(pattern_word, state)?;
1773 let matched_pattern = if state.shopt_opts.nocasematch {
1774 if state.shopt_opts.extglob {
1775 crate::interpreter::pattern::extglob_match_nocase(&pattern, &value)
1776 } else {
1777 crate::interpreter::pattern::glob_match_nocase(&pattern, &value)
1778 }
1779 } else if state.shopt_opts.extglob {
1780 crate::interpreter::pattern::extglob_match(&pattern, &value)
1781 } else {
1782 crate::interpreter::pattern::glob_match(&pattern, &value)
1783 };
1784 if matched_pattern {
1785 m = true;
1786 break;
1787 }
1788 }
1789 m
1790 };
1791
1792 if matched {
1793 if let Some(cmd) = &case_item.cmd {
1794 let r = execute_compound_list(cmd, state, stdin)?;
1795 result.stdout.push_str(&r.stdout);
1796 result.stderr.push_str(&r.stderr);
1797 result.exit_code = r.exit_code;
1798 }
1799
1800 match case_item.post_action {
1801 ast::CaseItemPostAction::ExitCase => break,
1802 ast::CaseItemPostAction::UnconditionallyExecuteNextCaseItem => {
1803 fall_through = true;
1805 i += 1;
1806 continue;
1807 }
1808 ast::CaseItemPostAction::ContinueEvaluatingCases => {
1809 i += 1;
1811 continue;
1812 }
1813 }
1814 }
1815 i += 1;
1816 }
1817
1818 Ok(result)
1819}
1820
1821pub(crate) fn clone_commands(
1826 commands: &HashMap<String, Arc<dyn crate::commands::VirtualCommand>>,
1827) -> HashMap<String, Arc<dyn crate::commands::VirtualCommand>> {
1828 commands.clone()
1829}
1830
1831fn make_exec_callback(
1839 state: &InterpreterState,
1840) -> impl Fn(&str) -> Result<CommandResult, RustBashError> {
1841 let cloned_fs = state.fs.deep_clone();
1842 let env = state.env.clone();
1843 let cwd = state.cwd.clone();
1844 let functions = state.functions.clone();
1845 let last_exit_code = state.last_exit_code;
1846 let commands = clone_commands(&state.commands);
1847 let shell_opts = state.shell_opts.clone();
1848 let shopt_opts = state.shopt_opts.clone();
1849 let limits = state.limits.clone();
1850 let network_policy = state.network_policy.clone();
1851 let positional_params = state.positional_params.clone();
1852 let shell_name = state.shell_name.clone();
1853 let random_seed = state.random_seed;
1854 let start_time = state.counters.start_time;
1855 let shell_start_time = state.shell_start_time;
1856 let last_argument = state.last_argument.clone();
1857 let call_stack = state.call_stack.clone();
1858 let machtype = state.machtype.clone();
1859 let hosttype = state.hosttype.clone();
1860
1861 move |cmd_str: &str| {
1862 let program = parse(cmd_str)?;
1863
1864 let sub_fs = cloned_fs.deep_clone();
1865
1866 let mut sub_state = InterpreterState {
1867 fs: sub_fs,
1868 env: env.clone(),
1869 cwd: cwd.clone(),
1870 functions: functions.clone(),
1871 last_exit_code,
1872 commands: clone_commands(&commands),
1873 shell_opts: shell_opts.clone(),
1874 shopt_opts: shopt_opts.clone(),
1875 limits: limits.clone(),
1876 counters: ExecutionCounters {
1877 command_count: 0,
1878 output_size: 0,
1879 start_time,
1880 substitution_depth: 0,
1881 call_depth: 0,
1882 },
1883 network_policy: network_policy.clone(),
1884 should_exit: false,
1885 loop_depth: 0,
1886 control_flow: None,
1887 positional_params: positional_params.clone(),
1888 shell_name: shell_name.clone(),
1889 random_seed,
1890 local_scopes: Vec::new(),
1891 in_function_depth: 0,
1892 traps: HashMap::new(),
1893 in_trap: false,
1894 errexit_suppressed: 0,
1895 stdin_offset: 0,
1896 dir_stack: Vec::new(),
1897 command_hash: HashMap::new(),
1898 aliases: HashMap::new(),
1899 current_lineno: 0,
1900 shell_start_time,
1901 last_argument: last_argument.clone(),
1902 call_stack: call_stack.clone(),
1903 machtype: machtype.clone(),
1904 hosttype: hosttype.clone(),
1905 persistent_fds: HashMap::new(),
1906 next_auto_fd: 10,
1907 proc_sub_counter: 0,
1908 proc_sub_prealloc: HashMap::new(),
1909 pipe_stdin_bytes: None,
1910 pending_cmdsub_stderr: String::new(),
1911 };
1912
1913 let result = execute_program(&program, &mut sub_state)?;
1914 Ok(CommandResult {
1915 stdout: result.stdout,
1916 stderr: result.stderr,
1917 exit_code: result.exit_code,
1918 stdout_bytes: None,
1919 })
1920 }
1921}
1922
1923fn execute_function_call(
1926 name: &str,
1927 args: &[String],
1928 state: &mut InterpreterState,
1929) -> Result<ExecResult, RustBashError> {
1930 use crate::interpreter::ControlFlow;
1931
1932 state.counters.call_depth += 1;
1934 if state.counters.call_depth > state.limits.max_call_depth {
1935 let actual = state.counters.call_depth;
1936 state.counters.call_depth -= 1;
1937 return Err(RustBashError::LimitExceeded {
1938 limit_name: "max_call_depth",
1939 limit_value: state.limits.max_call_depth,
1940 actual_value: actual,
1941 });
1942 }
1943
1944 let func_def = state.functions.get(name).unwrap().clone();
1946
1947 let saved_params = std::mem::replace(&mut state.positional_params, args.to_vec());
1949
1950 state.call_stack.push(CallFrame {
1953 func_name: name.to_string(),
1954 source: String::new(),
1955 lineno: state.current_lineno,
1956 });
1957
1958 state.local_scopes.push(HashMap::new());
1960 state.in_function_depth += 1;
1961
1962 let result = execute_compound_command(&func_def.body.0, func_def.body.1.as_ref(), state, "");
1964
1965 let exit_code = match state.control_flow.take() {
1967 Some(ControlFlow::Return(code)) => code,
1968 Some(other) => {
1969 state.control_flow = Some(other);
1971 result.as_ref().map(|r| r.exit_code).unwrap_or(1)
1972 }
1973 None => result.as_ref().map(|r| r.exit_code).unwrap_or(1),
1974 };
1975
1976 state.call_stack.pop();
1978
1979 state.in_function_depth -= 1;
1981 if let Some(restore_map) = state.local_scopes.pop() {
1982 for (var_name, old_value) in restore_map {
1983 match old_value {
1984 Some(var) => {
1985 state.env.insert(var_name, var);
1986 }
1987 None => {
1988 state.env.remove(&var_name);
1989 }
1990 }
1991 }
1992 }
1993
1994 state.positional_params = saved_params;
1996
1997 state.counters.call_depth -= 1;
1998
1999 let mut result = result?;
2000 result.exit_code = exit_code;
2001 Ok(result)
2002}
2003
2004fn dispatch_command(
2005 name: &str,
2006 args: &[String],
2007 state: &mut InterpreterState,
2008 stdin: &str,
2009) -> Result<ExecResult, RustBashError> {
2010 state.counters.command_count += 1;
2011 check_limits(state)?;
2012
2013 if args.first().map(|a| a.as_str()) == Some("--help") {
2015 if let Some(meta) = builtins::builtin_meta(name)
2017 && meta.supports_help_flag
2018 {
2019 return Ok(ExecResult {
2020 stdout: crate::commands::format_help(meta),
2021 stderr: String::new(),
2022 exit_code: 0,
2023 stdout_bytes: None,
2024 });
2025 }
2026 if let Some(cmd) = state.commands.get(name)
2028 && let Some(meta) = cmd.meta()
2029 && meta.supports_help_flag
2030 {
2031 return Ok(ExecResult {
2032 stdout: crate::commands::format_help(meta),
2033 stderr: String::new(),
2034 exit_code: 0,
2035 stdout_bytes: None,
2036 });
2037 }
2038 }
2040
2041 if let Some(result) = builtins::execute_builtin(name, args, state, stdin)? {
2043 return Ok(result);
2044 }
2045
2046 if state.functions.contains_key(name) {
2048 return execute_function_call(name, args, state);
2049 }
2050
2051 if let Some(cmd) = state.commands.get(name) {
2053 let env: HashMap<String, String> = state
2054 .env
2055 .iter()
2056 .map(|(k, v)| (k.clone(), v.value.as_scalar().to_string()))
2057 .collect();
2058 let vars_clone = state.env.clone();
2060 let fs = Arc::clone(&state.fs);
2061 let cwd = state.cwd.clone();
2062 let limits = state.limits.clone();
2063 let network_policy = state.network_policy.clone();
2064
2065 let binary_stdin = state.pipe_stdin_bytes.take();
2067 let exec_callback = make_exec_callback(state);
2068
2069 let ctx = CommandContext {
2070 fs: &*fs,
2071 cwd: &cwd,
2072 env: &env,
2073 variables: Some(&vars_clone),
2074 stdin,
2075 stdin_bytes: binary_stdin.as_deref(),
2076 limits: &limits,
2077 network_policy: &network_policy,
2078 exec: Some(&exec_callback),
2079 shell_opts: Some(&state.shell_opts),
2080 };
2081
2082 let effective_args: Vec<String>;
2084 let cmd_args: &[String] = if name == "echo" && state.shopt_opts.xpg_echo {
2085 effective_args = std::iter::once("-e".to_string())
2086 .chain(args.iter().cloned())
2087 .collect();
2088 &effective_args
2089 } else {
2090 args
2091 };
2092
2093 let cmd_result = cmd.execute(cmd_args, &ctx);
2094 return Ok(ExecResult {
2095 stdout: cmd_result.stdout,
2096 stderr: cmd_result.stderr,
2097 exit_code: cmd_result.exit_code,
2098 stdout_bytes: cmd_result.stdout_bytes,
2099 });
2100 }
2101
2102 Ok(ExecResult {
2104 stdout: String::new(),
2105 stderr: format!("{name}: command not found\n"),
2106 exit_code: 127,
2107 stdout_bytes: None,
2108 })
2109}
2110
2111fn extract_fd_varname(arg: &str) -> Option<&str> {
2116 let trimmed = arg.strip_prefix('{')?.strip_suffix('}')?;
2117 if !trimmed.is_empty()
2118 && trimmed
2119 .chars()
2120 .next()
2121 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
2122 && trimmed
2123 .chars()
2124 .all(|c| c.is_ascii_alphanumeric() || c == '_')
2125 {
2126 Some(trimmed)
2127 } else {
2128 None
2129 }
2130}
2131
2132fn execute_exec_builtin(
2137 args: &[String],
2138 redirects: &[&ast::IoRedirect],
2139 state: &mut InterpreterState,
2140 stdin: &str,
2141) -> Result<ExecResult, RustBashError> {
2142 if let Some(first_arg) = args.first()
2144 && let Some(varname) = extract_fd_varname(first_arg)
2145 {
2146 return exec_fd_variable_alloc(varname, args.get(1..), redirects, state);
2147 }
2148
2149 if args.is_empty() {
2151 return exec_persistent_redirects(redirects, state);
2152 }
2153
2154 let effective_stdin = match get_stdin_from_redirects(redirects, state, stdin) {
2156 Ok(s) => s,
2157 Err(RustBashError::RedirectFailed(msg)) => {
2158 let result = ExecResult {
2159 stderr: format!("rust-bash: {msg}\n"),
2160 exit_code: 1,
2161 ..ExecResult::default()
2162 };
2163 state.last_exit_code = 1;
2164 state.should_exit = true;
2165 return Ok(result);
2166 }
2167 Err(e) => return Err(e),
2168 };
2169 let mut result = dispatch_command(&args[0], &args[1..], state, &effective_stdin)?;
2170 apply_output_redirects(redirects, &mut result, state)?;
2171 state.last_exit_code = result.exit_code;
2172 state.should_exit = true;
2173 Ok(result)
2174}
2175
2176fn exec_persistent_redirects(
2178 redirects: &[&ast::IoRedirect],
2179 state: &mut InterpreterState,
2180) -> Result<ExecResult, RustBashError> {
2181 for redir in redirects {
2182 match redir {
2183 ast::IoRedirect::File(fd, kind, target) => {
2184 let filename = match redirect_target_filename(target, state) {
2185 Ok(f) => f,
2186 Err(RustBashError::RedirectFailed(msg)) => {
2187 return Ok(ExecResult {
2188 stderr: format!("rust-bash: {msg}\n"),
2189 exit_code: 1,
2190 ..ExecResult::default()
2191 });
2192 }
2193 Err(e) => return Err(e),
2194 };
2195 let path = resolve_path(&state.cwd, &filename);
2196 match kind {
2197 ast::IoFileRedirectKind::Write | ast::IoFileRedirectKind::Clobber => {
2198 let fd_num = fd.unwrap_or(1);
2199 if is_dev_null(&path) {
2200 state.persistent_fds.insert(fd_num, PersistentFd::DevNull);
2201 } else if is_dev_stdout(&path) {
2202 state.persistent_fds.remove(&fd_num);
2204 } else if is_dev_stderr(&path) {
2205 state.persistent_fds.remove(&fd_num);
2207 } else {
2208 state
2210 .fs
2211 .write_file(Path::new(&path), b"")
2212 .map_err(|e| RustBashError::Execution(e.to_string()))?;
2213 state
2214 .persistent_fds
2215 .insert(fd_num, PersistentFd::OutputFile(path));
2216 }
2217 }
2218 ast::IoFileRedirectKind::Append => {
2219 let fd_num = fd.unwrap_or(1);
2220 if is_dev_null(&path) {
2221 state.persistent_fds.insert(fd_num, PersistentFd::DevNull);
2222 } else if is_dev_stdout(&path) || is_dev_stderr(&path) {
2223 state.persistent_fds.remove(&fd_num);
2224 } else {
2225 state
2226 .persistent_fds
2227 .insert(fd_num, PersistentFd::OutputFile(path));
2228 }
2229 }
2230 ast::IoFileRedirectKind::Read => {
2231 let fd_num = fd.unwrap_or(0);
2232 if is_dev_null(&path) {
2233 state.persistent_fds.insert(fd_num, PersistentFd::DevNull);
2234 } else {
2235 state
2236 .persistent_fds
2237 .insert(fd_num, PersistentFd::InputFile(path));
2238 }
2239 }
2240 ast::IoFileRedirectKind::ReadAndWrite => {
2241 let fd_num = fd.unwrap_or(0);
2242 if !state.fs.exists(Path::new(&path)) {
2243 state
2244 .fs
2245 .write_file(Path::new(&path), b"")
2246 .map_err(|e| RustBashError::Execution(e.to_string()))?;
2247 }
2248 state
2249 .persistent_fds
2250 .insert(fd_num, PersistentFd::ReadWriteFile(path));
2251 }
2252 ast::IoFileRedirectKind::DuplicateOutput => {
2253 let fd_num = fd.unwrap_or(1);
2254 let dup_target = redirect_target_filename(target, state)?;
2255 if dup_target == "-" {
2257 state.persistent_fds.insert(fd_num, PersistentFd::Closed);
2258 } else if let Some(stripped) = dup_target.strip_suffix('-') {
2259 if let Ok(source_fd) = stripped.parse::<i32>() {
2261 if let Some(entry) = state.persistent_fds.get(&source_fd).cloned() {
2262 state.persistent_fds.insert(fd_num, entry);
2263 }
2264 state.persistent_fds.insert(source_fd, PersistentFd::Closed);
2265 }
2266 } else if let Ok(target_fd) = dup_target.parse::<i32>() {
2267 if let Some(entry) = state.persistent_fds.get(&target_fd).cloned() {
2269 state.persistent_fds.insert(fd_num, entry);
2270 } else if target_fd == 0 || target_fd == 1 || target_fd == 2 {
2271 state
2273 .persistent_fds
2274 .insert(fd_num, PersistentFd::DupStdFd(target_fd));
2275 } else {
2276 state.persistent_fds.remove(&fd_num);
2277 }
2278 }
2279 }
2280 ast::IoFileRedirectKind::DuplicateInput => {
2281 let fd_num = fd.unwrap_or(0);
2282 let dup_target = redirect_target_filename(target, state)?;
2283 if dup_target == "-" {
2284 state.persistent_fds.insert(fd_num, PersistentFd::Closed);
2285 }
2286 }
2287 }
2288 }
2289 ast::IoRedirect::OutputAndError(word, _append) => {
2290 let filename = expand_word_to_string_mut(word, state)?;
2291 let path = resolve_path(&state.cwd, &filename);
2292 if is_dev_null(&path) {
2293 state.persistent_fds.insert(1, PersistentFd::DevNull);
2294 state.persistent_fds.insert(2, PersistentFd::DevNull);
2295 } else {
2296 let pfd = PersistentFd::OutputFile(path);
2297 state.persistent_fds.insert(1, pfd.clone());
2298 state.persistent_fds.insert(2, pfd);
2299 }
2300 }
2301 _ => {}
2302 }
2303 }
2304 Ok(ExecResult::default())
2305}
2306
2307fn exec_fd_variable_alloc(
2309 varname: &str,
2310 extra_args: Option<&[String]>,
2311 redirects: &[&ast::IoRedirect],
2312 state: &mut InterpreterState,
2313) -> Result<ExecResult, RustBashError> {
2314 let is_close = redirects.iter().any(|r| {
2316 matches!(
2317 r,
2318 ast::IoRedirect::File(_, ast::IoFileRedirectKind::DuplicateOutput, ast::IoFileRedirectTarget::Duplicate(w)) if w.value == "-"
2319 )
2320 });
2321
2322 if is_close {
2323 if let Some(var) = state.env.get(varname)
2325 && let Ok(fd_num) = var.value.as_scalar().parse::<i32>()
2326 {
2327 state.persistent_fds.insert(fd_num, PersistentFd::Closed);
2328 }
2329 return Ok(ExecResult::default());
2330 }
2331
2332 if extra_args.is_some_and(|a| !a.is_empty()) {
2334 return Ok(ExecResult {
2335 stderr: "rust-bash: exec: too many arguments\n".to_string(),
2336 exit_code: 1,
2337 ..Default::default()
2338 });
2339 }
2340
2341 let fd_num = state.next_auto_fd;
2343 state.next_auto_fd += 1;
2344
2345 set_variable(state, varname, fd_num.to_string())?;
2347
2348 for redir in redirects {
2350 if let ast::IoRedirect::File(_fd, kind, target) = redir {
2351 let filename = redirect_target_filename(target, state)?;
2352 let path = resolve_path(&state.cwd, &filename);
2353 match kind {
2354 ast::IoFileRedirectKind::Write | ast::IoFileRedirectKind::Clobber => {
2355 if is_dev_null(&path) {
2356 state.persistent_fds.insert(fd_num, PersistentFd::DevNull);
2357 } else if is_dev_stdout(&path) || is_dev_stderr(&path) {
2358 state.persistent_fds.remove(&fd_num);
2359 } else {
2360 state
2361 .fs
2362 .write_file(Path::new(&path), b"")
2363 .map_err(|e| RustBashError::Execution(e.to_string()))?;
2364 state
2365 .persistent_fds
2366 .insert(fd_num, PersistentFd::OutputFile(path));
2367 }
2368 }
2369 ast::IoFileRedirectKind::Append => {
2370 if is_dev_null(&path) {
2371 state.persistent_fds.insert(fd_num, PersistentFd::DevNull);
2372 } else if is_dev_stdout(&path) || is_dev_stderr(&path) {
2373 state.persistent_fds.remove(&fd_num);
2374 } else {
2375 state
2376 .persistent_fds
2377 .insert(fd_num, PersistentFd::OutputFile(path));
2378 }
2379 }
2380 ast::IoFileRedirectKind::Read => {
2381 if is_dev_null(&path) {
2382 state.persistent_fds.insert(fd_num, PersistentFd::DevNull);
2383 } else {
2384 state
2385 .persistent_fds
2386 .insert(fd_num, PersistentFd::InputFile(path));
2387 }
2388 }
2389 ast::IoFileRedirectKind::ReadAndWrite => {
2390 if is_dev_null(&path) {
2391 state.persistent_fds.insert(fd_num, PersistentFd::DevNull);
2392 } else {
2393 if !state.fs.exists(Path::new(&path)) {
2394 state
2395 .fs
2396 .write_file(Path::new(&path), b"")
2397 .map_err(|e| RustBashError::Execution(e.to_string()))?;
2398 }
2399 state
2400 .persistent_fds
2401 .insert(fd_num, PersistentFd::ReadWriteFile(path));
2402 }
2403 }
2404 _ => {}
2405 }
2406 break; }
2408 }
2409
2410 Ok(ExecResult::default())
2411}
2412
2413fn is_dev_stdout(path: &str) -> bool {
2416 path == "/dev/stdout"
2417}
2418
2419fn is_dev_stderr(path: &str) -> bool {
2420 path == "/dev/stderr"
2421}
2422
2423fn is_dev_stdin(path: &str) -> bool {
2424 path == "/dev/stdin"
2425}
2426
2427fn is_dev_zero(path: &str) -> bool {
2428 path == "/dev/zero"
2429}
2430
2431fn is_dev_full(path: &str) -> bool {
2432 path == "/dev/full"
2433}
2434
2435fn is_special_dev_path(path: &str) -> bool {
2436 is_dev_null(path)
2437 || is_dev_stdout(path)
2438 || is_dev_stderr(path)
2439 || is_dev_stdin(path)
2440 || is_dev_zero(path)
2441 || is_dev_full(path)
2442}
2443
2444fn get_stdin_from_redirects(
2445 redirects: &[&ast::IoRedirect],
2446 state: &mut InterpreterState,
2447 default_stdin: &str,
2448) -> Result<String, RustBashError> {
2449 for redir in redirects {
2450 match redir {
2451 ast::IoRedirect::File(fd, kind, target) => {
2452 let fd_num = fd.unwrap_or(0);
2453 if fd_num == 0
2454 && matches!(
2455 kind,
2456 ast::IoFileRedirectKind::Read | ast::IoFileRedirectKind::ReadAndWrite
2457 )
2458 {
2459 let filename = redirect_target_filename(target, state)?;
2460 let path = resolve_path(&state.cwd, &filename);
2461 if is_dev_stdin(&path) {
2462 return Ok(default_stdin.to_string());
2463 }
2464 if is_dev_null(&path) || is_dev_zero(&path) || is_dev_full(&path) {
2465 return Ok(String::new());
2466 }
2467 if filename.is_empty() {
2469 return Err(RustBashError::RedirectFailed(
2470 ": No such file or directory".to_string(),
2471 ));
2472 }
2473 let content = state.fs.read_file(Path::new(&path)).map_err(|_| {
2474 RustBashError::RedirectFailed(format!(
2475 "{filename}: No such file or directory"
2476 ))
2477 })?;
2478 return Ok(String::from_utf8_lossy(&content).to_string());
2479 }
2480 if fd_num == 0 && matches!(kind, ast::IoFileRedirectKind::DuplicateInput) {
2482 let dup_target = redirect_target_filename(target, state)?;
2483 if let Ok(source_fd) = dup_target.parse::<i32>()
2484 && let Some(pfd) = state.persistent_fds.get(&source_fd)
2485 {
2486 match pfd {
2487 PersistentFd::InputFile(path) | PersistentFd::ReadWriteFile(path) => {
2488 let content = state
2489 .fs
2490 .read_file(Path::new(path))
2491 .map_err(|e| RustBashError::Execution(e.to_string()))?;
2492 return Ok(String::from_utf8_lossy(&content).to_string());
2493 }
2494 PersistentFd::DevNull | PersistentFd::Closed => {
2495 return Ok(String::new());
2496 }
2497 PersistentFd::OutputFile(_) | PersistentFd::DupStdFd(_) => {}
2498 }
2499 }
2500 }
2501 }
2502 ast::IoRedirect::HereString(fd, word) => {
2503 let fd_num = fd.unwrap_or(0);
2504 if fd_num == 0 {
2505 let val = expand_word_to_string_mut(word, state)?;
2506 if val.len() > state.limits.max_heredoc_size {
2507 return Err(RustBashError::LimitExceeded {
2508 limit_name: "max_heredoc_size",
2509 limit_value: state.limits.max_heredoc_size,
2510 actual_value: val.len(),
2511 });
2512 }
2513 return Ok(format!("{val}\n"));
2514 }
2515 }
2516 ast::IoRedirect::HereDocument(fd, heredoc) => {
2517 let fd_num = fd.unwrap_or(0);
2518 if fd_num == 0 {
2519 let body = if heredoc.requires_expansion {
2520 expand_word_to_string_mut(&heredoc.doc, state)?
2521 } else {
2522 heredoc.doc.value.clone()
2523 };
2524 if body.len() > state.limits.max_heredoc_size {
2525 return Err(RustBashError::LimitExceeded {
2526 limit_name: "max_heredoc_size",
2527 limit_value: state.limits.max_heredoc_size,
2528 actual_value: body.len(),
2529 });
2530 }
2531 if heredoc.remove_tabs {
2532 return Ok(body
2533 .lines()
2534 .map(|l| l.trim_start_matches('\t'))
2535 .collect::<Vec<_>>()
2536 .join("\n")
2537 + if body.ends_with('\n') { "\n" } else { "" });
2538 }
2539 return Ok(body);
2540 }
2541 }
2542 _ => {}
2543 }
2544 }
2545 Ok(default_stdin.to_string())
2546}
2547
2548fn apply_output_redirects(
2549 redirects: &[&ast::IoRedirect],
2550 result: &mut ExecResult,
2551 state: &mut InterpreterState,
2552) -> Result<(), RustBashError> {
2553 let mut redirected_fds = std::collections::HashSet::new();
2555 let mut deferred_errors: Vec<String> = Vec::new();
2558
2559 for redir in redirects {
2560 match redir {
2561 ast::IoRedirect::File(fd, kind, target) => {
2562 let fd_num = match kind {
2563 ast::IoFileRedirectKind::Read
2564 | ast::IoFileRedirectKind::ReadAndWrite
2565 | ast::IoFileRedirectKind::DuplicateInput => fd.unwrap_or(0),
2566 _ => fd.unwrap_or(1),
2567 };
2568 redirected_fds.insert(fd_num);
2569 let cont =
2570 apply_file_redirect(*fd, kind, target, result, state, &mut deferred_errors)?;
2571 if !cont {
2572 break;
2573 }
2574 }
2575 ast::IoRedirect::OutputAndError(word, append) => {
2576 redirected_fds.insert(1);
2577 redirected_fds.insert(2);
2578 let filename = expand_word_to_string_mut(word, state)?;
2579 if filename.is_empty() {
2580 result
2581 .stderr
2582 .push_str("rust-bash: : No such file or directory\n");
2583 result.exit_code = 1;
2584 break;
2585 }
2586 let path = resolve_path(&state.cwd, &filename);
2587
2588 if state.shell_opts.noclobber
2590 && !*append
2591 && !is_dev_null(&path)
2592 && state.fs.exists(Path::new(&path))
2593 {
2594 result.stderr.push_str(&format!(
2595 "rust-bash: {filename}: cannot overwrite existing file\n"
2596 ));
2597 result.stdout.clear();
2598 result.exit_code = 1;
2599 break;
2600 }
2601
2602 let combined = format!("{}{}", result.stdout, result.stderr);
2603
2604 if is_dev_null(&path) {
2605 result.stdout.clear();
2606 result.stderr.clear();
2607 } else if *append {
2608 write_or_append(state, &path, &combined, true)?;
2609 result.stdout.clear();
2610 result.stderr.clear();
2611 } else {
2612 write_or_append(state, &path, &combined, false)?;
2613 result.stdout.clear();
2614 result.stderr.clear();
2615 }
2616 }
2617 _ => {} }
2619 }
2620
2621 apply_persistent_fd_fallback(result, state, &redirected_fds)?;
2623
2624 for err in deferred_errors {
2628 result.stderr.push_str(&err);
2629 }
2630
2631 Ok(())
2632}
2633
2634fn apply_persistent_fd_fallback(
2636 result: &mut ExecResult,
2637 state: &InterpreterState,
2638 redirected_fds: &std::collections::HashSet<i32>,
2639) -> Result<(), RustBashError> {
2640 if !redirected_fds.contains(&1)
2642 && let Some(pfd) = state.persistent_fds.get(&1)
2643 {
2644 match pfd {
2645 PersistentFd::OutputFile(path) => {
2646 if !result.stdout.is_empty() {
2647 write_or_append(state, path, &result.stdout, true)?;
2648 result.stdout.clear();
2649 }
2650 }
2651 PersistentFd::DevNull | PersistentFd::Closed => {
2652 result.stdout.clear();
2653 }
2654 _ => {}
2655 }
2656 }
2657
2658 if !redirected_fds.contains(&2)
2660 && let Some(pfd) = state.persistent_fds.get(&2)
2661 {
2662 match pfd {
2663 PersistentFd::OutputFile(path) => {
2664 if !result.stderr.is_empty() {
2665 write_or_append(state, path, &result.stderr, true)?;
2666 result.stderr.clear();
2667 }
2668 }
2669 PersistentFd::DevNull | PersistentFd::Closed => {
2670 result.stderr.clear();
2671 }
2672 _ => {}
2673 }
2674 }
2675
2676 Ok(())
2677}
2678
2679fn apply_file_redirect(
2682 fd: Option<i32>,
2683 kind: &ast::IoFileRedirectKind,
2684 target: &ast::IoFileRedirectTarget,
2685 result: &mut ExecResult,
2686 state: &mut InterpreterState,
2687 deferred_errors: &mut Vec<String>,
2688) -> Result<bool, RustBashError> {
2689 macro_rules! try_filename {
2691 ($target:expr, $state:expr, $result:expr) => {
2692 match redirect_target_filename($target, $state) {
2693 Ok(f) => f,
2694 Err(RustBashError::RedirectFailed(msg)) => {
2695 let fd_num = fd.unwrap_or(1);
2697 if fd_num == 1 {
2698 $result.stdout.clear();
2699 } else if fd_num == 2 {
2700 $result.stderr.clear();
2701 }
2702 $result.stderr.push_str(&format!("rust-bash: {msg}\n"));
2703 $result.exit_code = 1;
2704 return Ok(false);
2705 }
2706 Err(e) => return Err(e),
2707 }
2708 };
2709 }
2710
2711 match kind {
2712 ast::IoFileRedirectKind::Write | ast::IoFileRedirectKind::Clobber => {
2713 let fd_num = fd.unwrap_or(1);
2714 let filename = try_filename!(target, state, result);
2715 let path = resolve_path(&state.cwd, &filename);
2716
2717 if state.shell_opts.noclobber
2719 && matches!(kind, ast::IoFileRedirectKind::Write)
2720 && !is_dev_null(&path)
2721 && !is_special_dev_path(&path)
2722 && state.fs.exists(Path::new(&path))
2723 {
2724 result.stderr.push_str(&format!(
2725 "rust-bash: {filename}: cannot overwrite existing file\n"
2726 ));
2727 if fd_num == 1 {
2728 result.stdout.clear();
2729 }
2730 result.exit_code = 1;
2731 return Ok(false);
2732 }
2733
2734 apply_write_redirect(fd_num, &path, result, state, false, deferred_errors)?;
2735 }
2736 ast::IoFileRedirectKind::Append => {
2737 let fd_num = fd.unwrap_or(1);
2738 let filename = try_filename!(target, state, result);
2739 let path = resolve_path(&state.cwd, &filename);
2740 apply_write_redirect(fd_num, &path, result, state, true, deferred_errors)?;
2741 }
2742 ast::IoFileRedirectKind::DuplicateOutput => {
2743 let fd_num = fd.unwrap_or(1);
2744 if !apply_duplicate_output(fd_num, target, result, state)? {
2745 return Ok(false);
2746 }
2747 }
2748 ast::IoFileRedirectKind::DuplicateInput => {
2749 let fd_num = fd.unwrap_or(0);
2750 if fd_num == 0 {
2751 } else {
2753 if !apply_duplicate_output(fd_num, target, result, state)? {
2755 return Ok(false);
2756 }
2757 }
2758 }
2759 ast::IoFileRedirectKind::Read => {
2760 }
2762 ast::IoFileRedirectKind::ReadAndWrite => {
2763 let fd_num = fd.unwrap_or(0);
2764 let filename = try_filename!(target, state, result);
2765 let path = resolve_path(&state.cwd, &filename);
2766 if !state.fs.exists(Path::new(&path)) {
2767 state
2768 .fs
2769 .write_file(Path::new(&path), b"")
2770 .map_err(|e| RustBashError::Execution(e.to_string()))?;
2771 }
2772 if fd_num == 1 {
2775 write_or_append(state, &path, &result.stdout, false)?;
2776 result.stdout.clear();
2777 } else if fd_num == 2 {
2778 write_or_append(state, &path, &result.stderr, false)?;
2779 result.stderr.clear();
2780 }
2781 }
2782 }
2783 Ok(true)
2784}
2785
2786fn apply_write_redirect(
2788 fd_num: i32,
2789 path: &str,
2790 result: &mut ExecResult,
2791 state: &InterpreterState,
2792 append: bool,
2793 deferred_errors: &mut Vec<String>,
2794) -> Result<(), RustBashError> {
2795 if is_dev_null(path) || is_dev_zero(path) {
2796 if fd_num == 1 {
2797 result.stdout.clear();
2798 result.stdout_bytes = None;
2799 } else if fd_num == 2 {
2800 result.stderr.clear();
2801 }
2802 } else if is_dev_stdout(path) {
2803 if fd_num == 2 {
2805 result.stdout.push_str(&result.stderr);
2806 result.stderr.clear();
2807 }
2808 } else if is_dev_stderr(path) {
2809 if fd_num == 1 {
2811 result.stderr.push_str(&result.stdout);
2812 result.stdout.clear();
2813 }
2814 } else if is_dev_full(path) {
2815 deferred_errors
2819 .push("rust-bash: write error: /dev/full: No space left on device\n".to_string());
2820 if fd_num == 1 {
2821 result.stdout.clear();
2822 } else if fd_num == 2 {
2823 result.stderr.clear();
2824 }
2825 result.exit_code = 1;
2826 } else {
2827 let p = Path::new(path);
2829 if state.fs.exists(p)
2830 && let Ok(meta) = state.fs.stat(p)
2831 && meta.node_type == crate::vfs::NodeType::Directory
2832 {
2833 let basename = path.rsplit('/').next().unwrap_or(path);
2834 let display = if basename.is_empty() { path } else { basename };
2835 deferred_errors.push(format!("rust-bash: {display}: Is a directory\n"));
2836 if fd_num == 1 {
2837 result.stdout.clear();
2838 } else if fd_num == 2 {
2839 result.stderr.clear();
2840 }
2841 result.exit_code = 1;
2842 return Ok(());
2843 }
2844 let content_bytes: Vec<u8> = if fd_num == 1 {
2845 if let Some(bytes) = result.stdout_bytes.take() {
2847 bytes
2848 } else {
2849 result.stdout.as_bytes().to_vec()
2850 }
2851 } else if fd_num == 2 {
2852 result.stderr.as_bytes().to_vec()
2853 } else {
2854 return write_to_persistent_fd(fd_num, result, state);
2855 };
2856 write_or_append_bytes(state, path, &content_bytes, append)?;
2857 if fd_num == 1 {
2858 result.stdout.clear();
2859 result.stdout_bytes = None;
2860 } else if fd_num == 2 {
2861 result.stderr.clear();
2862 }
2863 }
2864 Ok(())
2865}
2866
2867fn write_to_persistent_fd(
2869 _fd_num: i32,
2870 _result: &mut ExecResult,
2871 _state: &InterpreterState,
2872) -> Result<(), RustBashError> {
2873 Ok(())
2875}
2876
2877fn apply_duplicate_output(
2880 fd_num: i32,
2881 target: &ast::IoFileRedirectTarget,
2882 result: &mut ExecResult,
2883 state: &mut InterpreterState,
2884) -> Result<bool, RustBashError> {
2885 let dup_target_str = match target {
2886 ast::IoFileRedirectTarget::Duplicate(word) => expand_word_to_string_mut(word, state)?,
2887 ast::IoFileRedirectTarget::Fd(target_fd) => target_fd.to_string(),
2888 _ => return Ok(true),
2889 };
2890
2891 if dup_target_str == "-" {
2893 if fd_num == 1 {
2894 result.stdout.clear();
2895 } else if fd_num == 2 {
2896 result.stderr.clear();
2897 }
2898 return Ok(true);
2899 }
2900
2901 if let Some(source_str) = dup_target_str.strip_suffix('-') {
2903 if let Ok(source_fd) = source_str.parse::<i32>() {
2904 apply_dup_fd(fd_num, source_fd, result, state)?;
2906 if source_fd == 1 {
2908 result.stdout.clear();
2909 } else if source_fd == 2 {
2910 result.stderr.clear();
2911 } else {
2912 state.persistent_fds.insert(source_fd, PersistentFd::Closed);
2913 }
2914 }
2915 return Ok(true);
2916 }
2917
2918 if let Ok(target_fd) = dup_target_str.parse::<i32>() {
2920 if target_fd != 0
2922 && target_fd != 1
2923 && target_fd != 2
2924 && !state.persistent_fds.contains_key(&target_fd)
2925 {
2926 if fd_num == 1 {
2927 result.stdout.clear();
2928 }
2929 result
2930 .stderr
2931 .push_str(&format!("rust-bash: {fd_num}: Bad file descriptor\n"));
2932 result.exit_code = 1;
2933 return Ok(false);
2934 }
2935 apply_dup_fd(fd_num, target_fd, result, state)?;
2936 }
2937 Ok(true)
2938}
2939
2940fn apply_dup_fd(
2942 fd_num: i32,
2943 target_fd: i32,
2944 result: &mut ExecResult,
2945 state: &InterpreterState,
2946) -> Result<(), RustBashError> {
2947 if target_fd == 1 && fd_num == 2 {
2949 result.stdout.push_str(&result.stderr);
2951 result.stderr.clear();
2952 } else if target_fd == 2 && fd_num == 1 {
2953 result.stderr.push_str(&result.stdout);
2955 result.stdout.clear();
2956 } else if fd_num == 1 || fd_num == 2 {
2957 if let Some(pfd) = state.persistent_fds.get(&target_fd) {
2959 match pfd {
2960 PersistentFd::OutputFile(path) => {
2961 let content = if fd_num == 1 {
2962 let c = result.stdout.clone();
2963 result.stdout.clear();
2964 c
2965 } else {
2966 let c = result.stderr.clone();
2967 result.stderr.clear();
2968 c
2969 };
2970 write_or_append(state, path, &content, true)?;
2971 }
2972 PersistentFd::DevNull | PersistentFd::Closed => {
2973 if fd_num == 1 {
2974 result.stdout.clear();
2975 } else {
2976 result.stderr.clear();
2977 }
2978 }
2979 PersistentFd::DupStdFd(std_fd) => {
2980 if *std_fd == 1 && fd_num == 2 {
2982 result.stdout.push_str(&result.stderr);
2983 result.stderr.clear();
2984 } else if *std_fd == 2 && fd_num == 1 {
2985 result.stderr.push_str(&result.stdout);
2986 result.stdout.clear();
2987 }
2988 }
2990 _ => {}
2991 }
2992 }
2993 }
2994 Ok(())
2995}
2996
2997fn expand_process_substitution<'a>(
3002 kind: &ast::ProcessSubstitutionKind,
3003 list: &'a ast::CompoundList,
3004 state: &mut InterpreterState,
3005 deferred_write_subs: &mut Vec<(&'a ast::CompoundList, String)>,
3006) -> Result<String, RustBashError> {
3007 match kind {
3008 ast::ProcessSubstitutionKind::Read => execute_read_process_substitution(list, state),
3009 ast::ProcessSubstitutionKind::Write => {
3010 let path = allocate_proc_sub_temp_file(state, b"")?;
3011 deferred_write_subs.push((list, path.clone()));
3012 Ok(path)
3013 }
3014 }
3015}
3016
3017fn redirect_target_filename(
3018 target: &ast::IoFileRedirectTarget,
3019 state: &mut InterpreterState,
3020) -> Result<String, RustBashError> {
3021 match target {
3022 ast::IoFileRedirectTarget::Filename(word) => {
3023 let filename = expand_word_to_string_mut(word, state)?;
3024 if filename.is_empty() {
3025 return Err(RustBashError::RedirectFailed(
3026 ": No such file or directory".to_string(),
3027 ));
3028 }
3029 Ok(filename)
3030 }
3031 ast::IoFileRedirectTarget::Fd(fd) => Ok(fd.to_string()),
3032 ast::IoFileRedirectTarget::Duplicate(word) => expand_word_to_string_mut(word, state),
3033 ast::IoFileRedirectTarget::ProcessSubstitution(_, _) => {
3036 let key = std::ptr::from_ref(target) as usize;
3038 state.proc_sub_prealloc.remove(&key).ok_or_else(|| {
3039 RustBashError::Execution(
3040 "process substitution: no pre-allocated path available".into(),
3041 )
3042 })
3043 }
3044 }
3045}
3046
3047fn execute_read_process_substitution(
3050 list: &ast::CompoundList,
3051 state: &mut InterpreterState,
3052) -> Result<String, RustBashError> {
3053 let mut sub_state = make_proc_sub_state(state);
3054 let result = execute_compound_list(list, &mut sub_state, "")?;
3055
3056 state.counters.command_count = sub_state.counters.command_count;
3058 state.counters.output_size = sub_state.counters.output_size;
3059 state.proc_sub_counter = sub_state.proc_sub_counter;
3060
3061 allocate_proc_sub_temp_file(state, result.stdout.as_bytes())
3062}
3063
3064fn allocate_proc_sub_temp_file(
3066 state: &mut InterpreterState,
3067 content: &[u8],
3068) -> Result<String, RustBashError> {
3069 let path = format!("/tmp/.proc_sub_{}", state.proc_sub_counter);
3070 state.proc_sub_counter += 1;
3071
3072 let tmp = Path::new("/tmp");
3074 if !state.fs.exists(tmp) {
3075 state
3076 .fs
3077 .mkdir_p(tmp)
3078 .map_err(|e| RustBashError::Execution(e.to_string()))?;
3079 }
3080
3081 state
3082 .fs
3083 .write_file(Path::new(&path), content)
3084 .map_err(|e| RustBashError::Execution(e.to_string()))?;
3085
3086 Ok(path)
3087}
3088
3089fn make_proc_sub_state(state: &mut InterpreterState) -> InterpreterState {
3093 InterpreterState {
3094 fs: Arc::clone(&state.fs),
3095 env: state.env.clone(),
3096 cwd: state.cwd.clone(),
3097 functions: state.functions.clone(),
3098 last_exit_code: state.last_exit_code,
3099 commands: clone_commands(&state.commands),
3100 shell_opts: state.shell_opts.clone(),
3101 shopt_opts: state.shopt_opts.clone(),
3102 limits: state.limits.clone(),
3103 counters: ExecutionCounters {
3104 command_count: state.counters.command_count,
3105 output_size: state.counters.output_size,
3106 start_time: state.counters.start_time,
3107 substitution_depth: state.counters.substitution_depth,
3108 call_depth: 0,
3109 },
3110 network_policy: state.network_policy.clone(),
3111 should_exit: false,
3112 loop_depth: 0,
3113 control_flow: None,
3114 positional_params: state.positional_params.clone(),
3115 shell_name: state.shell_name.clone(),
3116 random_seed: state.random_seed,
3117 local_scopes: Vec::new(),
3118 in_function_depth: 0,
3119 traps: HashMap::new(),
3120 in_trap: false,
3121 errexit_suppressed: 0,
3122 stdin_offset: 0,
3123 dir_stack: state.dir_stack.clone(),
3124 command_hash: state.command_hash.clone(),
3125 aliases: state.aliases.clone(),
3126 current_lineno: state.current_lineno,
3127 shell_start_time: state.shell_start_time,
3128 last_argument: state.last_argument.clone(),
3129 call_stack: state.call_stack.clone(),
3130 machtype: state.machtype.clone(),
3131 hosttype: state.hosttype.clone(),
3132 persistent_fds: HashMap::new(),
3133 next_auto_fd: 10,
3134 proc_sub_counter: state.proc_sub_counter,
3135 proc_sub_prealloc: HashMap::new(),
3136 pipe_stdin_bytes: None,
3137 pending_cmdsub_stderr: String::new(),
3138 }
3139}
3140
3141fn is_dev_null(path: &str) -> bool {
3142 path == "/dev/null"
3143}
3144
3145fn write_or_append(
3146 state: &InterpreterState,
3147 path: &str,
3148 content: &str,
3149 append: bool,
3150) -> Result<(), RustBashError> {
3151 write_or_append_bytes(state, path, content.as_bytes(), append)
3152}
3153
3154fn write_or_append_bytes(
3155 state: &InterpreterState,
3156 path: &str,
3157 content: &[u8],
3158 append: bool,
3159) -> Result<(), RustBashError> {
3160 let p = Path::new(path);
3161
3162 if append {
3163 if state.fs.exists(p) {
3164 state
3165 .fs
3166 .append_file(p, content)
3167 .map_err(|e| RustBashError::Execution(e.to_string()))?;
3168 } else {
3169 state
3170 .fs
3171 .write_file(p, content)
3172 .map_err(|e| RustBashError::Execution(e.to_string()))?;
3173 }
3174 } else {
3175 state
3176 .fs
3177 .write_file(p, content)
3178 .map_err(|e| RustBashError::Execution(e.to_string()))?;
3179 }
3180 Ok(())
3181}
3182
3183fn execute_extended_test(
3186 expr: &ast::ExtendedTestExpr,
3187 state: &mut InterpreterState,
3188) -> Result<ExecResult, RustBashError> {
3189 let should_trace = state.shell_opts.xtrace;
3190 let mut exec_result = match eval_extended_test_expr(expr, state) {
3191 Ok(result) => ExecResult {
3192 exit_code: if result { 0 } else { 1 },
3193 ..ExecResult::default()
3194 },
3195 Err(RustBashError::Execution(ref msg)) => {
3196 let exit_code = if msg.contains("invalid regex") { 2 } else { 1 };
3197 state.last_exit_code = exit_code;
3198 ExecResult {
3199 stderr: format!("rust-bash: {msg}\n"),
3200 exit_code,
3201 ..ExecResult::default()
3202 }
3203 }
3204 Err(e) => return Err(e),
3205 };
3206 if should_trace {
3207 let repr = format_extended_test_expr_expanded(expr, state);
3208 let ps4 = expand_ps4(state);
3209 exec_result.stderr = format!("{ps4}[[ {repr} ]]\n{}", exec_result.stderr);
3210 }
3211 Ok(exec_result)
3212}
3213
3214fn format_extended_test_expr_expanded(
3216 expr: &ast::ExtendedTestExpr,
3217 state: &mut InterpreterState,
3218) -> String {
3219 match expr {
3220 ast::ExtendedTestExpr::And(l, r) => {
3221 format!(
3222 "{} && {}",
3223 format_extended_test_expr_expanded(l, state),
3224 format_extended_test_expr_expanded(r, state)
3225 )
3226 }
3227 ast::ExtendedTestExpr::Or(l, r) => {
3228 format!(
3229 "{} || {}",
3230 format_extended_test_expr_expanded(l, state),
3231 format_extended_test_expr_expanded(r, state)
3232 )
3233 }
3234 ast::ExtendedTestExpr::Not(inner) => {
3235 format!("! {}", format_extended_test_expr_expanded(inner, state))
3236 }
3237 ast::ExtendedTestExpr::Parenthesized(inner) => {
3238 format_extended_test_expr_expanded(inner, state)
3239 }
3240 ast::ExtendedTestExpr::UnaryTest(pred, word) => {
3241 let expanded = expand_word_to_string_mut(word, state).unwrap_or_default();
3242 format!("{} {}", format_unary_pred(pred), expanded)
3243 }
3244 ast::ExtendedTestExpr::BinaryTest(pred, l, r) => {
3245 let l_exp = expand_word_to_string_mut(l, state).unwrap_or_default();
3246 let r_exp = expand_word_to_string_mut(r, state).unwrap_or_default();
3247 format!("{} {} {}", l_exp, format_binary_pred(pred), r_exp)
3248 }
3249 }
3250}
3251
3252fn format_unary_pred(pred: &ast::UnaryPredicate) -> &'static str {
3253 use brush_parser::ast::UnaryPredicate;
3254 match pred {
3255 UnaryPredicate::FileExists => "-a",
3256 UnaryPredicate::FileExistsAndIsBlockSpecialFile => "-b",
3257 UnaryPredicate::FileExistsAndIsCharSpecialFile => "-c",
3258 UnaryPredicate::FileExistsAndIsDir => "-d",
3259 UnaryPredicate::FileExistsAndIsRegularFile => "-f",
3260 UnaryPredicate::FileExistsAndIsSetgid => "-g",
3261 UnaryPredicate::FileExistsAndIsSymlink => "-h",
3262 UnaryPredicate::FileExistsAndHasStickyBit => "-k",
3263 UnaryPredicate::FileExistsAndIsFifo => "-p",
3264 UnaryPredicate::FileExistsAndIsReadable => "-r",
3265 UnaryPredicate::FileExistsAndIsNotZeroLength => "-s",
3266 UnaryPredicate::FdIsOpenTerminal => "-t",
3267 UnaryPredicate::FileExistsAndIsSetuid => "-u",
3268 UnaryPredicate::FileExistsAndIsWritable => "-w",
3269 UnaryPredicate::FileExistsAndIsExecutable => "-x",
3270 UnaryPredicate::FileExistsAndOwnedByEffectiveGroupId => "-G",
3271 UnaryPredicate::FileExistsAndModifiedSinceLastRead => "-N",
3272 UnaryPredicate::FileExistsAndOwnedByEffectiveUserId => "-O",
3273 UnaryPredicate::FileExistsAndIsSocket => "-S",
3274 UnaryPredicate::StringHasZeroLength => "-z",
3275 UnaryPredicate::StringHasNonZeroLength => "-n",
3276 UnaryPredicate::ShellOptionEnabled => "-o",
3277 UnaryPredicate::ShellVariableIsSetAndAssigned => "-v",
3278 UnaryPredicate::ShellVariableIsSetAndNameRef => "-R",
3279 }
3280}
3281
3282fn format_binary_pred(pred: &ast::BinaryPredicate) -> &'static str {
3283 use brush_parser::ast::BinaryPredicate;
3284 match pred {
3285 BinaryPredicate::StringExactlyMatchesPattern => "==",
3286 BinaryPredicate::StringDoesNotExactlyMatchPattern => "!=",
3287 BinaryPredicate::StringExactlyMatchesString => "==",
3288 BinaryPredicate::StringDoesNotExactlyMatchString => "!=",
3289 BinaryPredicate::StringMatchesRegex => "=~",
3290 BinaryPredicate::StringContainsSubstring => "=~",
3291 BinaryPredicate::ArithmeticEqualTo => "-eq",
3292 BinaryPredicate::ArithmeticNotEqualTo => "-ne",
3293 BinaryPredicate::ArithmeticLessThan => "-lt",
3294 BinaryPredicate::ArithmeticGreaterThan => "-gt",
3295 BinaryPredicate::ArithmeticLessThanOrEqualTo => "-le",
3296 BinaryPredicate::ArithmeticGreaterThanOrEqualTo => "-ge",
3297 BinaryPredicate::FilesReferToSameDeviceAndInodeNumbers => "-ef",
3298 BinaryPredicate::LeftFileIsNewerOrExistsWhenRightDoesNot => "-nt",
3299 BinaryPredicate::LeftFileIsOlderOrDoesNotExistWhenRightDoes => "-ot",
3300 _ => "?",
3301 }
3302}
3303
3304fn eval_extended_test_expr(
3305 expr: &ast::ExtendedTestExpr,
3306 state: &mut InterpreterState,
3307) -> Result<bool, RustBashError> {
3308 match expr {
3309 ast::ExtendedTestExpr::And(left, right) => {
3310 let l = eval_extended_test_expr(left, state)?;
3311 if !l {
3312 return Ok(false);
3313 }
3314 eval_extended_test_expr(right, state)
3315 }
3316 ast::ExtendedTestExpr::Or(left, right) => {
3317 let l = eval_extended_test_expr(left, state)?;
3318 if l {
3319 return Ok(true);
3320 }
3321 eval_extended_test_expr(right, state)
3322 }
3323 ast::ExtendedTestExpr::Not(inner) => {
3324 let val = eval_extended_test_expr(inner, state)?;
3325 Ok(!val)
3326 }
3327 ast::ExtendedTestExpr::Parenthesized(inner) => eval_extended_test_expr(inner, state),
3328 ast::ExtendedTestExpr::UnaryTest(pred, word) => {
3329 use brush_parser::ast::UnaryPredicate;
3330 if matches!(pred, UnaryPredicate::ShellVariableIsSetAndAssigned) {
3332 let operand = expand_word_to_string_mut(word, state)?;
3333 return Ok(test_variable_is_set(&operand, state));
3334 }
3335 let operand = expand_word_to_string_mut(word, state)?;
3336 let env: HashMap<String, String> = state
3337 .env
3338 .iter()
3339 .map(|(k, v)| (k.clone(), v.value.as_scalar().to_string()))
3340 .collect();
3341 Ok(crate::commands::test_cmd::eval_unary_predicate(
3342 pred,
3343 &operand,
3344 &*state.fs,
3345 &state.cwd,
3346 &env,
3347 Some(&state.shell_opts),
3348 ))
3349 }
3350 ast::ExtendedTestExpr::BinaryTest(pred, left_word, right_word) => {
3351 let left = expand_word_to_string_mut(left_word, state)?;
3352
3353 if matches!(
3355 pred,
3356 ast::BinaryPredicate::StringMatchesRegex
3357 | ast::BinaryPredicate::StringContainsSubstring
3358 ) {
3359 let raw = &right_word.value;
3362 let is_fully_quoted = is_word_fully_quoted(raw);
3363 let pattern = expand_word_to_string_mut(right_word, state)?;
3364 if is_fully_quoted {
3365 return Ok(left.contains(&pattern));
3366 }
3367 let effective_pattern = build_regex_with_quoted_literals(raw, state)?;
3369 return eval_regex_match(&left, &effective_pattern, state);
3370 }
3371
3372 let right = expand_word_to_string_mut(right_word, state)?;
3373
3374 use brush_parser::ast::BinaryPredicate;
3379 if matches!(
3380 pred,
3381 BinaryPredicate::ArithmeticEqualTo
3382 | BinaryPredicate::ArithmeticNotEqualTo
3383 | BinaryPredicate::ArithmeticLessThan
3384 | BinaryPredicate::ArithmeticGreaterThan
3385 | BinaryPredicate::ArithmeticLessThanOrEqualTo
3386 | BinaryPredicate::ArithmeticGreaterThanOrEqualTo
3387 ) {
3388 let lval =
3389 crate::commands::test_cmd::parse_bash_int_pub(&left).unwrap_or_else(|| {
3390 crate::interpreter::arithmetic::eval_arithmetic(&left, state).unwrap_or(0)
3391 });
3392 let rval =
3393 crate::commands::test_cmd::parse_bash_int_pub(&right).unwrap_or_else(|| {
3394 crate::interpreter::arithmetic::eval_arithmetic(&right, state).unwrap_or(0)
3395 });
3396 let result = match pred {
3397 BinaryPredicate::ArithmeticEqualTo => lval == rval,
3398 BinaryPredicate::ArithmeticNotEqualTo => lval != rval,
3399 BinaryPredicate::ArithmeticLessThan => lval < rval,
3400 BinaryPredicate::ArithmeticGreaterThan => lval > rval,
3401 BinaryPredicate::ArithmeticLessThanOrEqualTo => lval <= rval,
3402 BinaryPredicate::ArithmeticGreaterThanOrEqualTo => lval >= rval,
3403 _ => unreachable!(),
3404 };
3405 return Ok(result);
3406 }
3407
3408 if state.shopt_opts.nocasematch {
3411 let result = match pred {
3413 ast::BinaryPredicate::StringExactlyMatchesPattern => {
3414 crate::interpreter::pattern::extglob_match_nocase(&right, &left)
3415 }
3416 ast::BinaryPredicate::StringDoesNotExactlyMatchPattern => {
3417 !crate::interpreter::pattern::extglob_match_nocase(&right, &left)
3418 }
3419 ast::BinaryPredicate::StringExactlyMatchesString => {
3420 left.eq_ignore_ascii_case(&right)
3421 }
3422 ast::BinaryPredicate::StringDoesNotExactlyMatchString => {
3423 !left.eq_ignore_ascii_case(&right)
3424 }
3425 _ => crate::commands::test_cmd::eval_binary_predicate(
3426 pred, &left, &right, true, &*state.fs, &state.cwd,
3427 ),
3428 };
3429 Ok(result)
3430 } else {
3431 let result = match pred {
3433 ast::BinaryPredicate::StringExactlyMatchesPattern => {
3434 crate::interpreter::pattern::extglob_match(&right, &left)
3435 }
3436 ast::BinaryPredicate::StringDoesNotExactlyMatchPattern => {
3437 !crate::interpreter::pattern::extglob_match(&right, &left)
3438 }
3439 _ => crate::commands::test_cmd::eval_binary_predicate(
3440 pred, &left, &right, true, &*state.fs, &state.cwd,
3441 ),
3442 };
3443 Ok(result)
3444 }
3445 }
3446 }
3447}
3448
3449fn test_variable_is_set(operand: &str, state: &mut InterpreterState) -> bool {
3452 if let Some(bracket_pos) = operand.find('[')
3454 && operand.ends_with(']')
3455 {
3456 let name = &operand[..bracket_pos];
3457 let index = &operand[bracket_pos + 1..operand.len() - 1];
3458 let resolved = crate::interpreter::resolve_nameref_or_self(name, state);
3459
3460 if index == "@" || index == "*" {
3461 return state
3462 .env
3463 .get(&resolved)
3464 .is_some_and(|var| match &var.value {
3465 VariableValue::IndexedArray(map) => !map.is_empty(),
3466 VariableValue::AssociativeArray(map) => !map.is_empty(),
3467 _ => false,
3468 });
3469 }
3470
3471 let var_type = state.env.get(&resolved).map(|var| match &var.value {
3473 VariableValue::IndexedArray(_) => 0,
3474 VariableValue::AssociativeArray(_) => 1,
3475 VariableValue::Scalar(_) => 2,
3476 });
3477
3478 return match var_type {
3479 Some(0) => {
3480 let idx = eval_index_arithmetic(index, state);
3482 let Some(var) = state.env.get(&resolved) else {
3483 return false;
3484 };
3485 if let VariableValue::IndexedArray(map) = &var.value {
3486 let actual_idx = if idx < 0 {
3487 let max_key = map.keys().next_back().copied().unwrap_or(0);
3488 let resolved_idx = max_key as i64 + 1 + idx;
3489 if resolved_idx < 0 {
3490 return false;
3491 }
3492 resolved_idx as usize
3493 } else {
3494 idx as usize
3495 };
3496 map.contains_key(&actual_idx)
3497 } else {
3498 false
3499 }
3500 }
3501 Some(1) => {
3502 state
3504 .env
3505 .get(&resolved)
3506 .and_then(|var| {
3507 if let VariableValue::AssociativeArray(map) = &var.value {
3508 Some(map.contains_key(index))
3509 } else {
3510 None
3511 }
3512 })
3513 .unwrap_or(false)
3514 }
3515 Some(2) => {
3516 let idx = eval_index_arithmetic(index, state);
3518 idx == 0 || idx == -1
3519 }
3520 _ => false,
3521 };
3522 }
3523 let resolved = crate::interpreter::resolve_nameref_or_self(operand, state);
3525 state.env.contains_key(&resolved)
3526}
3527
3528fn eval_index_arithmetic(index: &str, state: &mut InterpreterState) -> i64 {
3531 crate::interpreter::arithmetic::eval_arithmetic(index, state)
3532 .unwrap_or_else(|_| crate::interpreter::expansion::simple_arith_eval(index, state))
3533}
3534
3535fn eval_regex_match(
3536 string: &str,
3537 pattern: &str,
3538 state: &mut InterpreterState,
3539) -> Result<bool, RustBashError> {
3540 let effective_pattern = if state.shopt_opts.nocasematch {
3542 format!("(?i){pattern}")
3543 } else {
3544 pattern.to_string()
3545 };
3546 let re = regex::Regex::new(&effective_pattern)
3547 .map_err(|e| RustBashError::Execution(format!("invalid regex '{pattern}': {e}")))?;
3548
3549 if let Some(captures) = re.captures(string) {
3550 let mut map = std::collections::BTreeMap::new();
3553 let whole = captures.get(0).map(|m| m.as_str()).unwrap_or("");
3554 map.insert(0, whole.to_string());
3555 for i in 1..captures.len() {
3556 let val = captures.get(i).map(|m| m.as_str()).unwrap_or("");
3557 map.insert(i, val.to_string());
3558 }
3559 state.env.insert(
3560 "BASH_REMATCH".to_string(),
3561 Variable {
3562 value: VariableValue::IndexedArray(map),
3563 attrs: VariableAttrs::empty(),
3564 },
3565 );
3566 Ok(true)
3567 } else {
3568 state.env.insert(
3570 "BASH_REMATCH".to_string(),
3571 Variable {
3572 value: VariableValue::IndexedArray(std::collections::BTreeMap::new()),
3573 attrs: VariableAttrs::empty(),
3574 },
3575 );
3576 Ok(false)
3577 }
3578}
3579
3580fn is_word_fully_quoted(raw: &str) -> bool {
3582 let trimmed = raw.trim();
3583 if trimmed.len() < 2 {
3584 return false;
3585 }
3586 if trimmed.starts_with('\'') && trimmed.ends_with('\'') {
3588 return true;
3589 }
3590 if trimmed.starts_with('"') && trimmed.ends_with('"') {
3592 return true;
3593 }
3594 if (trimmed.starts_with("$'") && trimmed.ends_with('\''))
3596 || (trimmed.starts_with("$\"") && trimmed.ends_with('"'))
3597 {
3598 return true;
3599 }
3600 false
3601}
3602
3603fn build_regex_with_quoted_literals(
3606 raw: &str,
3607 state: &mut InterpreterState,
3608) -> Result<String, RustBashError> {
3609 let mut result = String::new();
3610 let chars: Vec<char> = raw.chars().collect();
3611 let mut i = 0;
3612 while i < chars.len() {
3613 match chars[i] {
3614 '\'' => {
3615 i += 1;
3617 let mut literal = String::new();
3618 while i < chars.len() && chars[i] != '\'' {
3619 literal.push(chars[i]);
3620 i += 1;
3621 }
3622 if i < chars.len() {
3623 i += 1; }
3625 result.push_str(®ex::escape(&literal));
3626 }
3627 '"' => {
3628 i += 1;
3630 let mut content = String::new();
3631 while i < chars.len() && chars[i] != '"' {
3632 if chars[i] == '\\' && i + 1 < chars.len() {
3633 content.push(chars[i + 1]);
3634 i += 2;
3635 } else {
3636 content.push(chars[i]);
3637 i += 1;
3638 }
3639 }
3640 if i < chars.len() {
3641 i += 1; }
3643 let word = ast::Word {
3645 value: content,
3646 loc: None,
3647 };
3648 let expanded = expand_word_to_string_mut(&word, state)?;
3649 result.push_str(®ex::escape(&expanded));
3650 }
3651 '\\' if i + 1 < chars.len() => {
3652 result.push_str(®ex::escape(&chars[i + 1].to_string()));
3654 i += 2;
3655 }
3656 '$' => {
3657 let mut var_text = String::new();
3659 var_text.push('$');
3660 i += 1;
3661 if i < chars.len() && chars[i] == '{' {
3662 var_text.push('{');
3664 i += 1;
3665 let mut depth = 1;
3666 while i < chars.len() && depth > 0 {
3667 if chars[i] == '{' {
3668 depth += 1;
3669 } else if chars[i] == '}' {
3670 depth -= 1;
3671 }
3672 var_text.push(chars[i]);
3673 i += 1;
3674 }
3675 } else {
3676 while i < chars.len() && (chars[i].is_ascii_alphanumeric() || chars[i] == '_') {
3678 var_text.push(chars[i]);
3679 i += 1;
3680 }
3681 }
3682 let word = ast::Word {
3683 value: var_text,
3684 loc: None,
3685 };
3686 let expanded = expand_word_to_string_mut(&word, state)?;
3687 result.push_str(&expanded);
3688 }
3689 c => {
3690 result.push(c);
3691 i += 1;
3692 }
3693 }
3694 }
3695 Ok(result)
3696}