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 {
974 ast::AssignmentName::VariableName(n) => n.clone(),
975 ast::AssignmentName::ArrayElementName(n, _) => n.clone(),
976 };
977 match &assignment.value {
978 ast::AssignmentValue::Scalar(w) => {
979 let value = expand_word_to_string_mut(w, state)?;
980 args.push(format!("{name}={value}"));
981 }
982 ast::AssignmentValue::Array(items) => {
983 let mut parts = Vec::new();
984 for (opt_idx_word, val_word) in items {
985 let vals = expand_word_mut(val_word, state)?;
986 if let Some(idx_word) = opt_idx_word {
987 let idx_str = expand_word_to_string_mut(idx_word, state)?;
988 let first = vals.first().cloned().unwrap_or_default();
989 parts.push(format!("[{idx_str}]={first}"));
990 for v in vals.into_iter().skip(1) {
991 parts.push(v);
992 }
993 } else {
994 parts.extend(vals);
995 }
996 }
997 args.push(format!("{name}=({})", parts.join(" ")));
998 }
999 }
1000 }
1001 ast::CommandPrefixOrSuffixItem::ProcessSubstitution(kind, subshell) => {
1002 let path = expand_process_substitution(
1003 kind,
1004 &subshell.list,
1005 state,
1006 &mut deferred_write_subs,
1007 )?;
1008 proc_sub_temps.push(path.clone());
1009 args.push(path);
1010 }
1011 }
1012 }
1013 }
1014
1015 let Some(cmd_name) = cmd_name else {
1017 if state.shell_opts.xtrace && !assignments.is_empty() {
1019 let ps4 = expand_ps4(state);
1020 let mut trace = String::new();
1021 for a in &assignments {
1022 let part = match a {
1023 Assignment::Scalar { name, value } => format!("{name}={value}"),
1024 Assignment::IndexedArray { name, elements, .. } => {
1025 let vals: Vec<String> =
1026 elements.iter().map(|(_, v)| xtrace_quote(v)).collect();
1027 format!("{name}=({})", vals.join(" "))
1028 }
1029 Assignment::ArrayElement {
1030 name, index, value, ..
1031 } => format!("{name}[{index}]={value}"),
1032 Assignment::AppendArrayElement {
1033 name, index, value, ..
1034 } => format!("{name}[{index}]+={value}"),
1035 Assignment::AppendArray { name, elements, .. } => {
1036 let vals: Vec<String> =
1037 elements.iter().map(|(_, v)| xtrace_quote(v)).collect();
1038 format!("{name}+=({})", vals.join(" "))
1039 }
1040 Assignment::AssocArray { name, .. } => format!("{name}=(...)"),
1041 Assignment::AppendAssocArray { name, .. } => format!("{name}+=(...)"),
1042 Assignment::AppendScalar { name, value } => format!("{name}+={value}"),
1043 };
1044 trace.push_str(&format!("{ps4}{part}\n"));
1045 }
1046 let mut result = ExecResult {
1048 stderr: trace,
1049 ..ExecResult::default()
1050 };
1051 for a in assignments {
1052 apply_assignment_shell_error(a, state, &mut result)?;
1053 }
1054 return Ok(result);
1055 }
1056 let mut result = ExecResult::default();
1057 for a in assignments {
1058 apply_assignment_shell_error(a, state, &mut result)?;
1059 }
1060 return Ok(result);
1061 };
1062
1063 if cmd_name.is_empty() && args.is_empty() {
1065 let mut result = ExecResult {
1066 exit_code: state.last_exit_code,
1067 ..ExecResult::default()
1068 };
1069 for a in assignments {
1070 apply_assignment_shell_error(a, state, &mut result)?;
1071 }
1072 return Ok(result);
1073 }
1074
1075 let (cmd_name, args) = if state.shopt_opts.expand_aliases {
1078 if let Some(expansion) = state.aliases.get(&cmd_name).cloned() {
1079 let mut parts: Vec<String> = expansion
1080 .split_whitespace()
1081 .map(|s| s.to_string())
1082 .collect();
1083 if parts.is_empty() {
1084 (cmd_name, args)
1085 } else {
1086 let new_cmd = parts.remove(0);
1087 parts.extend(args);
1088 (new_cmd, parts)
1089 }
1090 } else {
1091 (cmd_name, args)
1092 }
1093 } else {
1094 (cmd_name, args)
1095 };
1096
1097 if cmd_name == "exec" {
1100 if args.first().map(|a| a.as_str()) == Some("--help")
1102 && let Some(meta) = builtins::builtin_meta("exec")
1103 && meta.supports_help_flag
1104 {
1105 return Ok(ExecResult {
1106 stdout: crate::commands::format_help(meta),
1107 stderr: String::new(),
1108 exit_code: 0,
1109 stdout_bytes: None,
1110 });
1111 }
1112 for a in &assignments {
1113 let mut dummy = ExecResult::default();
1114 apply_assignment_shell_error(a.clone(), state, &mut dummy)?;
1115 if dummy.exit_code != 0 {
1116 return Ok(dummy);
1117 }
1118 }
1119 return execute_exec_builtin(&args, &redirects, state, stdin);
1120 }
1121
1122 let mut saved: Vec<(String, Option<Variable>)> = Vec::new();
1127 let mut prefix_stderr = String::new();
1128 for a in &assignments {
1129 saved.push((a.name().to_string(), state.env.get(a.name()).cloned()));
1130 let mut dummy = ExecResult::default();
1131 apply_assignment_shell_error(a.clone(), state, &mut dummy)?;
1132 if dummy.exit_code != 0 {
1133 prefix_stderr.push_str(&dummy.stderr);
1134 }
1135 }
1136
1137 struct RedirProcSub<'a> {
1140 temp_path: String,
1141 kind: &'a ast::ProcessSubstitutionKind,
1142 list: &'a ast::CompoundList,
1143 }
1144 let last_arg = args.last().cloned().unwrap_or_else(|| cmd_name.clone());
1145 let should_trace = state.shell_opts.xtrace;
1146 let pre_ps4 = if should_trace {
1148 Some(expand_ps4(state))
1149 } else {
1150 None
1151 };
1152
1153 let mut inner_result = (|| -> Result<ExecResult, RustBashError> {
1154 let mut redir_proc_subs: Vec<RedirProcSub<'_>> = Vec::new();
1159 for redir in &redirects {
1160 if let ast::IoRedirect::File(
1161 _,
1162 _,
1163 target @ ast::IoFileRedirectTarget::ProcessSubstitution(kind, subshell),
1164 ) = redir
1165 {
1166 let temp_path = match kind {
1167 ast::ProcessSubstitutionKind::Read => {
1168 execute_read_process_substitution(&subshell.list, state)?
1169 }
1170 ast::ProcessSubstitutionKind::Write => allocate_proc_sub_temp_file(state, b"")?,
1171 };
1172 proc_sub_temps.push(temp_path.clone());
1173 let key = std::ptr::from_ref(target) as usize;
1176 state.proc_sub_prealloc.insert(key, temp_path.clone());
1177 redir_proc_subs.push(RedirProcSub {
1178 temp_path,
1179 kind,
1180 list: &subshell.list,
1181 });
1182 }
1183 }
1184
1185 let effective_stdin = match get_stdin_from_redirects(&redirects, state, stdin) {
1187 Ok(s) => s,
1188 Err(RustBashError::RedirectFailed(msg)) => {
1189 let mut result = ExecResult {
1190 stderr: format!("rust-bash: {msg}\n"),
1191 exit_code: 1,
1192 ..ExecResult::default()
1193 };
1194 state.last_exit_code = 1;
1195 apply_output_redirects(&redirects, &mut result, state)?;
1196 return Ok(result);
1197 }
1198 Err(e) => return Err(e),
1199 };
1200
1201 state.last_argument = last_arg.clone();
1203
1204 let mut result = dispatch_command(&cmd_name, &args, state, &effective_stdin)?;
1209
1210 if let Some(ref ps4) = pre_ps4 {
1212 let mut trace = format_xtrace_command(ps4, &cmd_name, &args);
1213 if matches!(
1217 cmd_name.as_str(),
1218 "readonly" | "declare" | "typeset" | "export"
1219 ) {
1220 for arg in &args {
1221 if let Some(eq_pos) = arg.find('=') {
1222 let name_part = &arg[..eq_pos];
1223 if !name_part.is_empty()
1225 && !name_part.starts_with('-')
1226 && name_part
1227 .chars()
1228 .all(|c| c.is_alphanumeric() || c == '_' || c == '+')
1229 {
1230 trace.push_str(&format!("{ps4}{arg}\n"));
1231 }
1232 }
1233 }
1234 }
1235 result.stderr = format!("{trace}{}", result.stderr);
1237 }
1238
1239 apply_output_redirects(&redirects, &mut result, state)?;
1241
1242 for rps in &redir_proc_subs {
1244 if matches!(rps.kind, ast::ProcessSubstitutionKind::Write) {
1245 let content = state
1246 .fs
1247 .read_file(Path::new(&rps.temp_path))
1248 .map_err(|e| RustBashError::Execution(e.to_string()))?;
1249 let stdin_data = String::from_utf8_lossy(&content).to_string();
1250 let mut sub_state = make_proc_sub_state(state);
1251 let inner_result = execute_compound_list(rps.list, &mut sub_state, &stdin_data)?;
1252 state.counters.command_count = sub_state.counters.command_count;
1253 state.counters.output_size = sub_state.counters.output_size;
1254 state.proc_sub_counter = sub_state.proc_sub_counter;
1255 result.stdout.push_str(&inner_result.stdout);
1256 result.stderr.push_str(&inner_result.stderr);
1257 }
1258 }
1259
1260 for (inner_list, temp_path) in &deferred_write_subs {
1262 let content = state
1263 .fs
1264 .read_file(Path::new(temp_path))
1265 .map_err(|e| RustBashError::Execution(e.to_string()))?;
1266 let stdin_data = String::from_utf8_lossy(&content).to_string();
1267 let mut sub_state = make_proc_sub_state(state);
1268 let inner_result = execute_compound_list(inner_list, &mut sub_state, &stdin_data)?;
1269 state.counters.command_count = sub_state.counters.command_count;
1270 state.counters.output_size = sub_state.counters.output_size;
1271 state.proc_sub_counter = sub_state.proc_sub_counter;
1272 result.stdout.push_str(&inner_result.stdout);
1273 result.stderr.push_str(&inner_result.stderr);
1274 }
1275
1276 Ok(result)
1277 })();
1278
1279 for temp_path in &proc_sub_temps {
1281 let _ = state.fs.remove_file(Path::new(temp_path));
1282 }
1283 state.proc_sub_prealloc.clear();
1284
1285 for (name, old_value) in saved {
1287 match old_value {
1288 Some(var) => {
1289 state.env.insert(name, var);
1290 }
1291 None => {
1292 state.env.remove(&name);
1293 }
1294 }
1295 }
1296
1297 if let Ok(ref mut r) = inner_result
1299 && !prefix_stderr.is_empty()
1300 {
1301 r.stderr = format!("{prefix_stderr}{}", r.stderr);
1302 }
1303
1304 inner_result
1305}
1306
1307fn execute_compound_command(
1310 compound: &ast::CompoundCommand,
1311 redirects: Option<&ast::RedirectList>,
1312 state: &mut InterpreterState,
1313 stdin: &str,
1314) -> Result<ExecResult, RustBashError> {
1315 let mut result = match compound {
1316 ast::CompoundCommand::IfClause(if_clause) => execute_if(if_clause, state, stdin)?,
1317 ast::CompoundCommand::ForClause(for_clause) => execute_for(for_clause, state, stdin)?,
1318 ast::CompoundCommand::WhileClause(wc) => execute_while_until(wc, false, state, stdin)?,
1319 ast::CompoundCommand::UntilClause(uc) => execute_while_until(uc, true, state, stdin)?,
1320 ast::CompoundCommand::BraceGroup(bg) => execute_compound_list(&bg.list, state, stdin)?,
1321 ast::CompoundCommand::Subshell(sub) => execute_subshell(&sub.list, state, stdin)?,
1322 ast::CompoundCommand::CaseClause(cc) => execute_case(cc, state, stdin)?,
1323 ast::CompoundCommand::Arithmetic(arith) => execute_arithmetic(arith, state)?,
1324 ast::CompoundCommand::ArithmeticForClause(afc) => {
1325 execute_arithmetic_for(afc, state, stdin)?
1326 }
1327 };
1328
1329 if let Some(redir_list) = redirects {
1331 let redir_refs: Vec<&ast::IoRedirect> = redir_list.0.iter().collect();
1332 apply_output_redirects(&redir_refs, &mut result, state)?;
1333 }
1334
1335 state.last_exit_code = result.exit_code;
1336 Ok(result)
1337}
1338
1339fn execute_if(
1340 if_clause: &ast::IfClauseCommand,
1341 state: &mut InterpreterState,
1342 stdin: &str,
1343) -> Result<ExecResult, RustBashError> {
1344 let mut result = ExecResult::default();
1345
1346 state.errexit_suppressed += 1;
1348 let cond = execute_compound_list(&if_clause.condition, state, stdin)?;
1349 state.errexit_suppressed -= 1;
1350 result.stdout.push_str(&cond.stdout);
1351 result.stderr.push_str(&cond.stderr);
1352
1353 if cond.exit_code == 0 {
1354 let body = execute_compound_list(&if_clause.then, state, stdin)?;
1355 result.stdout.push_str(&body.stdout);
1356 result.stderr.push_str(&body.stderr);
1357 result.exit_code = body.exit_code;
1358 return Ok(result);
1359 }
1360
1361 if let Some(elses) = &if_clause.elses {
1363 for else_clause in elses {
1364 if let Some(condition) = &else_clause.condition {
1365 state.errexit_suppressed += 1;
1367 let cond = execute_compound_list(condition, state, stdin)?;
1368 state.errexit_suppressed -= 1;
1369 result.stdout.push_str(&cond.stdout);
1370 result.stderr.push_str(&cond.stderr);
1371 if cond.exit_code == 0 {
1372 let body = execute_compound_list(&else_clause.body, state, stdin)?;
1373 result.stdout.push_str(&body.stdout);
1374 result.stderr.push_str(&body.stderr);
1375 result.exit_code = body.exit_code;
1376 return Ok(result);
1377 }
1378 } else {
1379 let body = execute_compound_list(&else_clause.body, state, stdin)?;
1381 result.stdout.push_str(&body.stdout);
1382 result.stderr.push_str(&body.stderr);
1383 result.exit_code = body.exit_code;
1384 return Ok(result);
1385 }
1386 }
1387 }
1388
1389 result.exit_code = 0;
1391 Ok(result)
1392}
1393
1394fn execute_for(
1395 for_clause: &ast::ForClauseCommand,
1396 state: &mut InterpreterState,
1397 stdin: &str,
1398) -> Result<ExecResult, RustBashError> {
1399 use crate::interpreter::ControlFlow;
1400
1401 let mut result = ExecResult::default();
1402
1403 let values: Vec<String> = if let Some(words) = &for_clause.values {
1404 let mut vals = Vec::new();
1405 for w in words {
1406 vals.extend(expand_word_mut(w, state)?);
1407 }
1408 vals
1409 } else {
1410 state.positional_params.clone()
1412 };
1413
1414 state.loop_depth += 1;
1415 let mut iterations: usize = 0;
1416 for val in &values {
1417 if state.should_exit {
1418 break;
1419 }
1420 iterations += 1;
1421 if iterations > state.limits.max_loop_iterations {
1422 state.loop_depth -= 1;
1423 return Err(RustBashError::LimitExceeded {
1424 limit_name: "max_loop_iterations",
1425 limit_value: state.limits.max_loop_iterations,
1426 actual_value: iterations,
1427 });
1428 }
1429
1430 set_variable(state, &for_clause.variable_name, val.clone())?;
1431 let r = execute_compound_list(&for_clause.body.list, state, stdin)?;
1432 result.stdout.push_str(&r.stdout);
1433 result.stderr.push_str(&r.stderr);
1434 result.exit_code = r.exit_code;
1435
1436 match state.control_flow.take() {
1437 Some(ControlFlow::Break(n)) => {
1438 if n > 1 {
1439 state.control_flow = Some(ControlFlow::Break(n - 1));
1440 }
1441 break;
1442 }
1443 Some(ControlFlow::Continue(n)) => {
1444 if n > 1 {
1445 state.control_flow = Some(ControlFlow::Continue(n - 1));
1446 break;
1447 }
1448 }
1450 Some(ret @ ControlFlow::Return(_)) => {
1451 state.control_flow = Some(ret);
1452 break;
1453 }
1454 None => {}
1455 }
1456 }
1457 state.loop_depth -= 1;
1458
1459 Ok(result)
1460}
1461
1462fn execute_arithmetic(
1463 arith: &ast::ArithmeticCommand,
1464 state: &mut InterpreterState,
1465) -> Result<ExecResult, RustBashError> {
1466 let val = crate::interpreter::arithmetic::eval_arithmetic(&arith.expr.value, state)?;
1467 let mut result = ExecResult {
1468 exit_code: if val != 0 { 0 } else { 1 },
1469 ..Default::default()
1470 };
1471 if state.shell_opts.xtrace {
1472 let ps4 = expand_ps4(state);
1473 result.stderr = format!(
1474 "{ps4}(({}))\n{}",
1475 arith.expr.value.trim_end(),
1476 result.stderr
1477 );
1478 }
1479 Ok(result)
1480}
1481
1482fn execute_arithmetic_for(
1483 afc: &ast::ArithmeticForClauseCommand,
1484 state: &mut InterpreterState,
1485 stdin: &str,
1486) -> Result<ExecResult, RustBashError> {
1487 use crate::interpreter::ControlFlow;
1488
1489 if let Some(init) = &afc.initializer {
1491 crate::interpreter::arithmetic::eval_arithmetic(&init.value, state)?;
1492 }
1493
1494 let mut result = ExecResult::default();
1495 let mut iterations: usize = 0;
1496
1497 state.loop_depth += 1;
1498 loop {
1499 if state.should_exit {
1500 break;
1501 }
1502 iterations += 1;
1503 if iterations > state.limits.max_loop_iterations {
1504 state.loop_depth -= 1;
1505 return Err(RustBashError::LimitExceeded {
1506 limit_name: "max_loop_iterations",
1507 limit_value: state.limits.max_loop_iterations,
1508 actual_value: iterations,
1509 });
1510 }
1511
1512 if let Some(cond) = &afc.condition {
1514 let val = crate::interpreter::arithmetic::eval_arithmetic(&cond.value, state)?;
1515 if val == 0 {
1516 break;
1517 }
1518 }
1519
1520 let body = execute_compound_list(&afc.body.list, state, stdin)?;
1522 result.stdout.push_str(&body.stdout);
1523 result.stderr.push_str(&body.stderr);
1524 result.exit_code = body.exit_code;
1525
1526 match state.control_flow.take() {
1527 Some(ControlFlow::Break(n)) => {
1528 if n > 1 {
1529 state.control_flow = Some(ControlFlow::Break(n - 1));
1530 }
1531 break;
1532 }
1533 Some(ControlFlow::Continue(n)) => {
1534 if n > 1 {
1535 state.control_flow = Some(ControlFlow::Continue(n - 1));
1536 break;
1537 }
1538 }
1539 Some(ret @ ControlFlow::Return(_)) => {
1540 state.control_flow = Some(ret);
1541 break;
1542 }
1543 None => {}
1544 }
1545
1546 if let Some(upd) = &afc.updater {
1548 crate::interpreter::arithmetic::eval_arithmetic(&upd.value, state)?;
1549 }
1550 }
1551 state.loop_depth -= 1;
1552
1553 Ok(result)
1554}
1555
1556fn execute_while_until(
1557 clause: &ast::WhileOrUntilClauseCommand,
1558 is_until: bool,
1559 state: &mut InterpreterState,
1560 stdin: &str,
1561) -> Result<ExecResult, RustBashError> {
1562 use crate::interpreter::ControlFlow;
1563
1564 let mut result = ExecResult::default();
1565 let mut iterations: usize = 0;
1566
1567 state.loop_depth += 1;
1568 loop {
1569 if state.should_exit {
1570 break;
1571 }
1572 iterations += 1;
1573 if iterations > state.limits.max_loop_iterations {
1574 state.loop_depth -= 1;
1575 return Err(RustBashError::LimitExceeded {
1576 limit_name: "max_loop_iterations",
1577 limit_value: state.limits.max_loop_iterations,
1578 actual_value: iterations,
1579 });
1580 }
1581
1582 state.errexit_suppressed += 1;
1584 let cond = execute_compound_list(&clause.0, state, stdin)?;
1585 state.errexit_suppressed -= 1;
1586 result.stdout.push_str(&cond.stdout);
1587 result.stderr.push_str(&cond.stderr);
1588
1589 let should_continue = if is_until {
1590 cond.exit_code != 0
1591 } else {
1592 cond.exit_code == 0
1593 };
1594
1595 if !should_continue {
1596 break;
1597 }
1598
1599 let body = execute_compound_list(&clause.1.list, state, stdin)?;
1600 result.stdout.push_str(&body.stdout);
1601 result.stderr.push_str(&body.stderr);
1602 result.exit_code = body.exit_code;
1603
1604 match state.control_flow.take() {
1605 Some(ControlFlow::Break(n)) => {
1606 if n > 1 {
1607 state.control_flow = Some(ControlFlow::Break(n - 1));
1608 }
1609 break;
1610 }
1611 Some(ControlFlow::Continue(n)) => {
1612 if n > 1 {
1613 state.control_flow = Some(ControlFlow::Continue(n - 1));
1614 break;
1615 }
1616 }
1618 Some(ret @ ControlFlow::Return(_)) => {
1619 state.control_flow = Some(ret);
1620 break;
1621 }
1622 None => {}
1623 }
1624 }
1625 state.loop_depth -= 1;
1626
1627 Ok(result)
1628}
1629
1630fn execute_subshell(
1631 list: &ast::CompoundList,
1632 state: &mut InterpreterState,
1633 stdin: &str,
1634) -> Result<ExecResult, RustBashError> {
1635 let cloned_fs = state.fs.deep_clone();
1637
1638 let mut sub_state = InterpreterState {
1639 fs: cloned_fs,
1640 env: state.env.clone(),
1641 cwd: state.cwd.clone(),
1642 functions: state.functions.clone(),
1643 last_exit_code: state.last_exit_code,
1644 commands: clone_commands(&state.commands),
1645 shell_opts: state.shell_opts.clone(),
1646 shopt_opts: state.shopt_opts.clone(),
1647 limits: state.limits.clone(),
1648 counters: ExecutionCounters {
1649 command_count: state.counters.command_count,
1650 output_size: state.counters.output_size,
1651 start_time: state.counters.start_time,
1652 substitution_depth: state.counters.substitution_depth,
1653 call_depth: 0,
1654 },
1655 network_policy: state.network_policy.clone(),
1656 should_exit: false,
1657 loop_depth: 0,
1658 control_flow: None,
1659 positional_params: state.positional_params.clone(),
1660 shell_name: state.shell_name.clone(),
1661 random_seed: state.random_seed,
1662 local_scopes: Vec::new(),
1663 in_function_depth: 0,
1664 traps: state.traps.clone(),
1665 in_trap: false,
1666 errexit_suppressed: 0,
1667 stdin_offset: 0,
1668 dir_stack: state.dir_stack.clone(),
1669 command_hash: state.command_hash.clone(),
1670 aliases: state.aliases.clone(),
1671 current_lineno: state.current_lineno,
1672 shell_start_time: state.shell_start_time,
1673 last_argument: state.last_argument.clone(),
1674 call_stack: state.call_stack.clone(),
1675 machtype: state.machtype.clone(),
1676 hosttype: state.hosttype.clone(),
1677 persistent_fds: state.persistent_fds.clone(),
1678 next_auto_fd: state.next_auto_fd,
1679 proc_sub_counter: state.proc_sub_counter,
1680 proc_sub_prealloc: HashMap::new(),
1681 pipe_stdin_bytes: None,
1682 };
1683
1684 let result = execute_compound_list(list, &mut sub_state, stdin);
1685
1686 state.counters.command_count = sub_state.counters.command_count;
1688 state.counters.output_size = sub_state.counters.output_size;
1689
1690 let result = result?;
1691
1692 Ok(result)
1694}
1695
1696fn execute_case(
1697 case_clause: &ast::CaseClauseCommand,
1698 state: &mut InterpreterState,
1699 stdin: &str,
1700) -> Result<ExecResult, RustBashError> {
1701 let value = expand_word_to_string_mut(&case_clause.value, state)?;
1702 let mut result = ExecResult::default();
1703
1704 let mut i = 0;
1705 let mut fall_through = false;
1706 while i < case_clause.cases.len() {
1707 let case_item = &case_clause.cases[i];
1708
1709 let matched = if fall_through {
1710 fall_through = false;
1711 true
1712 } else {
1713 let mut m = false;
1714 for pattern_word in &case_item.patterns {
1715 let pattern = expand_word_to_string_mut(pattern_word, state)?;
1716 let matched_pattern = if state.shopt_opts.nocasematch {
1717 if state.shopt_opts.extglob {
1718 crate::interpreter::pattern::extglob_match_nocase(&pattern, &value)
1719 } else {
1720 crate::interpreter::pattern::glob_match_nocase(&pattern, &value)
1721 }
1722 } else if state.shopt_opts.extglob {
1723 crate::interpreter::pattern::extglob_match(&pattern, &value)
1724 } else {
1725 crate::interpreter::pattern::glob_match(&pattern, &value)
1726 };
1727 if matched_pattern {
1728 m = true;
1729 break;
1730 }
1731 }
1732 m
1733 };
1734
1735 if matched {
1736 if let Some(cmd) = &case_item.cmd {
1737 let r = execute_compound_list(cmd, state, stdin)?;
1738 result.stdout.push_str(&r.stdout);
1739 result.stderr.push_str(&r.stderr);
1740 result.exit_code = r.exit_code;
1741 }
1742
1743 match case_item.post_action {
1744 ast::CaseItemPostAction::ExitCase => break,
1745 ast::CaseItemPostAction::UnconditionallyExecuteNextCaseItem => {
1746 fall_through = true;
1748 i += 1;
1749 continue;
1750 }
1751 ast::CaseItemPostAction::ContinueEvaluatingCases => {
1752 i += 1;
1754 continue;
1755 }
1756 }
1757 }
1758 i += 1;
1759 }
1760
1761 Ok(result)
1762}
1763
1764pub(crate) fn clone_commands(
1772 _commands: &HashMap<String, Box<dyn crate::commands::VirtualCommand>>,
1773) -> HashMap<String, Box<dyn crate::commands::VirtualCommand>> {
1774 crate::commands::register_default_commands()
1775}
1776
1777fn make_exec_callback(
1785 state: &InterpreterState,
1786) -> impl Fn(&str) -> Result<CommandResult, RustBashError> {
1787 let cloned_fs = state.fs.deep_clone();
1788 let env = state.env.clone();
1789 let cwd = state.cwd.clone();
1790 let functions = state.functions.clone();
1791 let last_exit_code = state.last_exit_code;
1792 let commands = clone_commands(&state.commands);
1793 let shell_opts = state.shell_opts.clone();
1794 let shopt_opts = state.shopt_opts.clone();
1795 let limits = state.limits.clone();
1796 let network_policy = state.network_policy.clone();
1797 let positional_params = state.positional_params.clone();
1798 let shell_name = state.shell_name.clone();
1799 let random_seed = state.random_seed;
1800 let start_time = state.counters.start_time;
1801 let shell_start_time = state.shell_start_time;
1802 let last_argument = state.last_argument.clone();
1803 let call_stack = state.call_stack.clone();
1804 let machtype = state.machtype.clone();
1805 let hosttype = state.hosttype.clone();
1806
1807 move |cmd_str: &str| {
1808 let program = parse(cmd_str)?;
1809
1810 let sub_fs = cloned_fs.deep_clone();
1811
1812 let mut sub_state = InterpreterState {
1813 fs: sub_fs,
1814 env: env.clone(),
1815 cwd: cwd.clone(),
1816 functions: functions.clone(),
1817 last_exit_code,
1818 commands: clone_commands(&commands),
1819 shell_opts: shell_opts.clone(),
1820 shopt_opts: shopt_opts.clone(),
1821 limits: limits.clone(),
1822 counters: ExecutionCounters {
1823 command_count: 0,
1824 output_size: 0,
1825 start_time,
1826 substitution_depth: 0,
1827 call_depth: 0,
1828 },
1829 network_policy: network_policy.clone(),
1830 should_exit: false,
1831 loop_depth: 0,
1832 control_flow: None,
1833 positional_params: positional_params.clone(),
1834 shell_name: shell_name.clone(),
1835 random_seed,
1836 local_scopes: Vec::new(),
1837 in_function_depth: 0,
1838 traps: HashMap::new(),
1839 in_trap: false,
1840 errexit_suppressed: 0,
1841 stdin_offset: 0,
1842 dir_stack: Vec::new(),
1843 command_hash: HashMap::new(),
1844 aliases: HashMap::new(),
1845 current_lineno: 0,
1846 shell_start_time,
1847 last_argument: last_argument.clone(),
1848 call_stack: call_stack.clone(),
1849 machtype: machtype.clone(),
1850 hosttype: hosttype.clone(),
1851 persistent_fds: HashMap::new(),
1852 next_auto_fd: 10,
1853 proc_sub_counter: 0,
1854 proc_sub_prealloc: HashMap::new(),
1855 pipe_stdin_bytes: None,
1856 };
1857
1858 let result = execute_program(&program, &mut sub_state)?;
1859 Ok(CommandResult {
1860 stdout: result.stdout,
1861 stderr: result.stderr,
1862 exit_code: result.exit_code,
1863 stdout_bytes: None,
1864 })
1865 }
1866}
1867
1868fn execute_function_call(
1871 name: &str,
1872 args: &[String],
1873 state: &mut InterpreterState,
1874) -> Result<ExecResult, RustBashError> {
1875 use crate::interpreter::ControlFlow;
1876
1877 state.counters.call_depth += 1;
1879 if state.counters.call_depth > state.limits.max_call_depth {
1880 let actual = state.counters.call_depth;
1881 state.counters.call_depth -= 1;
1882 return Err(RustBashError::LimitExceeded {
1883 limit_name: "max_call_depth",
1884 limit_value: state.limits.max_call_depth,
1885 actual_value: actual,
1886 });
1887 }
1888
1889 let func_def = state.functions.get(name).unwrap().clone();
1891
1892 let saved_params = std::mem::replace(&mut state.positional_params, args.to_vec());
1894
1895 state.call_stack.push(CallFrame {
1898 func_name: name.to_string(),
1899 source: String::new(),
1900 lineno: state.current_lineno,
1901 });
1902
1903 state.local_scopes.push(HashMap::new());
1905 state.in_function_depth += 1;
1906
1907 let result = execute_compound_command(&func_def.body.0, func_def.body.1.as_ref(), state, "");
1909
1910 let exit_code = match state.control_flow.take() {
1912 Some(ControlFlow::Return(code)) => code,
1913 Some(other) => {
1914 state.control_flow = Some(other);
1916 result.as_ref().map(|r| r.exit_code).unwrap_or(1)
1917 }
1918 None => result.as_ref().map(|r| r.exit_code).unwrap_or(1),
1919 };
1920
1921 state.call_stack.pop();
1923
1924 state.in_function_depth -= 1;
1926 if let Some(restore_map) = state.local_scopes.pop() {
1927 for (var_name, old_value) in restore_map {
1928 match old_value {
1929 Some(var) => {
1930 state.env.insert(var_name, var);
1931 }
1932 None => {
1933 state.env.remove(&var_name);
1934 }
1935 }
1936 }
1937 }
1938
1939 state.positional_params = saved_params;
1941
1942 state.counters.call_depth -= 1;
1943
1944 let mut result = result?;
1945 result.exit_code = exit_code;
1946 Ok(result)
1947}
1948
1949fn dispatch_command(
1950 name: &str,
1951 args: &[String],
1952 state: &mut InterpreterState,
1953 stdin: &str,
1954) -> Result<ExecResult, RustBashError> {
1955 state.counters.command_count += 1;
1956 check_limits(state)?;
1957
1958 if args.first().map(|a| a.as_str()) == Some("--help") {
1960 if let Some(meta) = builtins::builtin_meta(name)
1962 && meta.supports_help_flag
1963 {
1964 return Ok(ExecResult {
1965 stdout: crate::commands::format_help(meta),
1966 stderr: String::new(),
1967 exit_code: 0,
1968 stdout_bytes: None,
1969 });
1970 }
1971 if let Some(cmd) = state.commands.get(name)
1973 && let Some(meta) = cmd.meta()
1974 && meta.supports_help_flag
1975 {
1976 return Ok(ExecResult {
1977 stdout: crate::commands::format_help(meta),
1978 stderr: String::new(),
1979 exit_code: 0,
1980 stdout_bytes: None,
1981 });
1982 }
1983 }
1985
1986 if let Some(result) = builtins::execute_builtin(name, args, state, stdin)? {
1988 return Ok(result);
1989 }
1990
1991 if state.functions.contains_key(name) {
1993 return execute_function_call(name, args, state);
1994 }
1995
1996 if let Some(cmd) = state.commands.get(name) {
1998 let env: HashMap<String, String> = state
1999 .env
2000 .iter()
2001 .map(|(k, v)| (k.clone(), v.value.as_scalar().to_string()))
2002 .collect();
2003 let vars_clone = state.env.clone();
2005 let fs = Arc::clone(&state.fs);
2006 let cwd = state.cwd.clone();
2007 let limits = state.limits.clone();
2008 let network_policy = state.network_policy.clone();
2009
2010 let binary_stdin = state.pipe_stdin_bytes.take();
2012 let exec_callback = make_exec_callback(state);
2013
2014 let ctx = CommandContext {
2015 fs: &*fs,
2016 cwd: &cwd,
2017 env: &env,
2018 variables: Some(&vars_clone),
2019 stdin,
2020 stdin_bytes: binary_stdin.as_deref(),
2021 limits: &limits,
2022 network_policy: &network_policy,
2023 exec: Some(&exec_callback),
2024 shell_opts: Some(&state.shell_opts),
2025 };
2026
2027 let effective_args: Vec<String>;
2029 let cmd_args: &[String] = if name == "echo" && state.shopt_opts.xpg_echo {
2030 effective_args = std::iter::once("-e".to_string())
2031 .chain(args.iter().cloned())
2032 .collect();
2033 &effective_args
2034 } else {
2035 args
2036 };
2037
2038 let cmd_result = cmd.execute(cmd_args, &ctx);
2039 return Ok(ExecResult {
2040 stdout: cmd_result.stdout,
2041 stderr: cmd_result.stderr,
2042 exit_code: cmd_result.exit_code,
2043 stdout_bytes: cmd_result.stdout_bytes,
2044 });
2045 }
2046
2047 Ok(ExecResult {
2049 stdout: String::new(),
2050 stderr: format!("{name}: command not found\n"),
2051 exit_code: 127,
2052 stdout_bytes: None,
2053 })
2054}
2055
2056fn extract_fd_varname(arg: &str) -> Option<&str> {
2061 let trimmed = arg.strip_prefix('{')?.strip_suffix('}')?;
2062 if !trimmed.is_empty()
2063 && trimmed
2064 .chars()
2065 .next()
2066 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
2067 && trimmed
2068 .chars()
2069 .all(|c| c.is_ascii_alphanumeric() || c == '_')
2070 {
2071 Some(trimmed)
2072 } else {
2073 None
2074 }
2075}
2076
2077fn execute_exec_builtin(
2082 args: &[String],
2083 redirects: &[&ast::IoRedirect],
2084 state: &mut InterpreterState,
2085 stdin: &str,
2086) -> Result<ExecResult, RustBashError> {
2087 if let Some(first_arg) = args.first()
2089 && let Some(varname) = extract_fd_varname(first_arg)
2090 {
2091 return exec_fd_variable_alloc(varname, args.get(1..), redirects, state);
2092 }
2093
2094 if args.is_empty() {
2096 return exec_persistent_redirects(redirects, state);
2097 }
2098
2099 let effective_stdin = match get_stdin_from_redirects(redirects, state, stdin) {
2101 Ok(s) => s,
2102 Err(RustBashError::RedirectFailed(msg)) => {
2103 let result = ExecResult {
2104 stderr: format!("rust-bash: {msg}\n"),
2105 exit_code: 1,
2106 ..ExecResult::default()
2107 };
2108 state.last_exit_code = 1;
2109 state.should_exit = true;
2110 return Ok(result);
2111 }
2112 Err(e) => return Err(e),
2113 };
2114 let mut result = dispatch_command(&args[0], &args[1..], state, &effective_stdin)?;
2115 apply_output_redirects(redirects, &mut result, state)?;
2116 state.last_exit_code = result.exit_code;
2117 state.should_exit = true;
2118 Ok(result)
2119}
2120
2121fn exec_persistent_redirects(
2123 redirects: &[&ast::IoRedirect],
2124 state: &mut InterpreterState,
2125) -> Result<ExecResult, RustBashError> {
2126 for redir in redirects {
2127 match redir {
2128 ast::IoRedirect::File(fd, kind, target) => {
2129 let filename = match redirect_target_filename(target, state) {
2130 Ok(f) => f,
2131 Err(RustBashError::RedirectFailed(msg)) => {
2132 return Ok(ExecResult {
2133 stderr: format!("rust-bash: {msg}\n"),
2134 exit_code: 1,
2135 ..ExecResult::default()
2136 });
2137 }
2138 Err(e) => return Err(e),
2139 };
2140 let path = resolve_path(&state.cwd, &filename);
2141 match kind {
2142 ast::IoFileRedirectKind::Write | ast::IoFileRedirectKind::Clobber => {
2143 let fd_num = fd.unwrap_or(1);
2144 if is_dev_null(&path) {
2145 state.persistent_fds.insert(fd_num, PersistentFd::DevNull);
2146 } else if is_dev_stdout(&path) {
2147 state.persistent_fds.remove(&fd_num);
2149 } else if is_dev_stderr(&path) {
2150 state.persistent_fds.remove(&fd_num);
2152 } else {
2153 state
2155 .fs
2156 .write_file(Path::new(&path), b"")
2157 .map_err(|e| RustBashError::Execution(e.to_string()))?;
2158 state
2159 .persistent_fds
2160 .insert(fd_num, PersistentFd::OutputFile(path));
2161 }
2162 }
2163 ast::IoFileRedirectKind::Append => {
2164 let fd_num = fd.unwrap_or(1);
2165 if is_dev_null(&path) {
2166 state.persistent_fds.insert(fd_num, PersistentFd::DevNull);
2167 } else if is_dev_stdout(&path) || is_dev_stderr(&path) {
2168 state.persistent_fds.remove(&fd_num);
2169 } else {
2170 state
2171 .persistent_fds
2172 .insert(fd_num, PersistentFd::OutputFile(path));
2173 }
2174 }
2175 ast::IoFileRedirectKind::Read => {
2176 let fd_num = fd.unwrap_or(0);
2177 if is_dev_null(&path) {
2178 state.persistent_fds.insert(fd_num, PersistentFd::DevNull);
2179 } else {
2180 state
2181 .persistent_fds
2182 .insert(fd_num, PersistentFd::InputFile(path));
2183 }
2184 }
2185 ast::IoFileRedirectKind::ReadAndWrite => {
2186 let fd_num = fd.unwrap_or(0);
2187 if !state.fs.exists(Path::new(&path)) {
2188 state
2189 .fs
2190 .write_file(Path::new(&path), b"")
2191 .map_err(|e| RustBashError::Execution(e.to_string()))?;
2192 }
2193 state
2194 .persistent_fds
2195 .insert(fd_num, PersistentFd::ReadWriteFile(path));
2196 }
2197 ast::IoFileRedirectKind::DuplicateOutput => {
2198 let fd_num = fd.unwrap_or(1);
2199 let dup_target = redirect_target_filename(target, state)?;
2200 if dup_target == "-" {
2202 state.persistent_fds.insert(fd_num, PersistentFd::Closed);
2203 } else if let Some(stripped) = dup_target.strip_suffix('-') {
2204 if let Ok(source_fd) = stripped.parse::<i32>() {
2206 if let Some(entry) = state.persistent_fds.get(&source_fd).cloned() {
2207 state.persistent_fds.insert(fd_num, entry);
2208 }
2209 state.persistent_fds.insert(source_fd, PersistentFd::Closed);
2210 }
2211 } else if let Ok(target_fd) = dup_target.parse::<i32>() {
2212 if let Some(entry) = state.persistent_fds.get(&target_fd).cloned() {
2214 state.persistent_fds.insert(fd_num, entry);
2215 } else if target_fd == 0 || target_fd == 1 || target_fd == 2 {
2216 state
2218 .persistent_fds
2219 .insert(fd_num, PersistentFd::DupStdFd(target_fd));
2220 } else {
2221 state.persistent_fds.remove(&fd_num);
2222 }
2223 }
2224 }
2225 ast::IoFileRedirectKind::DuplicateInput => {
2226 let fd_num = fd.unwrap_or(0);
2227 let dup_target = redirect_target_filename(target, state)?;
2228 if dup_target == "-" {
2229 state.persistent_fds.insert(fd_num, PersistentFd::Closed);
2230 }
2231 }
2232 }
2233 }
2234 ast::IoRedirect::OutputAndError(word, _append) => {
2235 let filename = expand_word_to_string_mut(word, state)?;
2236 let path = resolve_path(&state.cwd, &filename);
2237 if is_dev_null(&path) {
2238 state.persistent_fds.insert(1, PersistentFd::DevNull);
2239 state.persistent_fds.insert(2, PersistentFd::DevNull);
2240 } else {
2241 let pfd = PersistentFd::OutputFile(path);
2242 state.persistent_fds.insert(1, pfd.clone());
2243 state.persistent_fds.insert(2, pfd);
2244 }
2245 }
2246 _ => {}
2247 }
2248 }
2249 Ok(ExecResult::default())
2250}
2251
2252fn exec_fd_variable_alloc(
2254 varname: &str,
2255 extra_args: Option<&[String]>,
2256 redirects: &[&ast::IoRedirect],
2257 state: &mut InterpreterState,
2258) -> Result<ExecResult, RustBashError> {
2259 let is_close = redirects.iter().any(|r| {
2261 matches!(
2262 r,
2263 ast::IoRedirect::File(_, ast::IoFileRedirectKind::DuplicateOutput, ast::IoFileRedirectTarget::Duplicate(w)) if w.value == "-"
2264 )
2265 });
2266
2267 if is_close {
2268 if let Some(var) = state.env.get(varname)
2270 && let Ok(fd_num) = var.value.as_scalar().parse::<i32>()
2271 {
2272 state.persistent_fds.insert(fd_num, PersistentFd::Closed);
2273 }
2274 return Ok(ExecResult::default());
2275 }
2276
2277 if extra_args.is_some_and(|a| !a.is_empty()) {
2279 return Ok(ExecResult {
2280 stderr: "rust-bash: exec: too many arguments\n".to_string(),
2281 exit_code: 1,
2282 ..Default::default()
2283 });
2284 }
2285
2286 let fd_num = state.next_auto_fd;
2288 state.next_auto_fd += 1;
2289
2290 set_variable(state, varname, fd_num.to_string())?;
2292
2293 for redir in redirects {
2295 if let ast::IoRedirect::File(_fd, kind, target) = redir {
2296 let filename = redirect_target_filename(target, state)?;
2297 let path = resolve_path(&state.cwd, &filename);
2298 match kind {
2299 ast::IoFileRedirectKind::Write | ast::IoFileRedirectKind::Clobber => {
2300 if is_dev_null(&path) {
2301 state.persistent_fds.insert(fd_num, PersistentFd::DevNull);
2302 } else if is_dev_stdout(&path) || is_dev_stderr(&path) {
2303 state.persistent_fds.remove(&fd_num);
2304 } else {
2305 state
2306 .fs
2307 .write_file(Path::new(&path), b"")
2308 .map_err(|e| RustBashError::Execution(e.to_string()))?;
2309 state
2310 .persistent_fds
2311 .insert(fd_num, PersistentFd::OutputFile(path));
2312 }
2313 }
2314 ast::IoFileRedirectKind::Append => {
2315 if is_dev_null(&path) {
2316 state.persistent_fds.insert(fd_num, PersistentFd::DevNull);
2317 } else if is_dev_stdout(&path) || is_dev_stderr(&path) {
2318 state.persistent_fds.remove(&fd_num);
2319 } else {
2320 state
2321 .persistent_fds
2322 .insert(fd_num, PersistentFd::OutputFile(path));
2323 }
2324 }
2325 ast::IoFileRedirectKind::Read => {
2326 if is_dev_null(&path) {
2327 state.persistent_fds.insert(fd_num, PersistentFd::DevNull);
2328 } else {
2329 state
2330 .persistent_fds
2331 .insert(fd_num, PersistentFd::InputFile(path));
2332 }
2333 }
2334 ast::IoFileRedirectKind::ReadAndWrite => {
2335 if is_dev_null(&path) {
2336 state.persistent_fds.insert(fd_num, PersistentFd::DevNull);
2337 } else {
2338 if !state.fs.exists(Path::new(&path)) {
2339 state
2340 .fs
2341 .write_file(Path::new(&path), b"")
2342 .map_err(|e| RustBashError::Execution(e.to_string()))?;
2343 }
2344 state
2345 .persistent_fds
2346 .insert(fd_num, PersistentFd::ReadWriteFile(path));
2347 }
2348 }
2349 _ => {}
2350 }
2351 break; }
2353 }
2354
2355 Ok(ExecResult::default())
2356}
2357
2358fn is_dev_stdout(path: &str) -> bool {
2361 path == "/dev/stdout"
2362}
2363
2364fn is_dev_stderr(path: &str) -> bool {
2365 path == "/dev/stderr"
2366}
2367
2368fn is_dev_stdin(path: &str) -> bool {
2369 path == "/dev/stdin"
2370}
2371
2372fn is_dev_zero(path: &str) -> bool {
2373 path == "/dev/zero"
2374}
2375
2376fn is_dev_full(path: &str) -> bool {
2377 path == "/dev/full"
2378}
2379
2380fn is_special_dev_path(path: &str) -> bool {
2381 is_dev_null(path)
2382 || is_dev_stdout(path)
2383 || is_dev_stderr(path)
2384 || is_dev_stdin(path)
2385 || is_dev_zero(path)
2386 || is_dev_full(path)
2387}
2388
2389fn get_stdin_from_redirects(
2390 redirects: &[&ast::IoRedirect],
2391 state: &mut InterpreterState,
2392 default_stdin: &str,
2393) -> Result<String, RustBashError> {
2394 for redir in redirects {
2395 match redir {
2396 ast::IoRedirect::File(fd, kind, target) => {
2397 let fd_num = fd.unwrap_or(0);
2398 if fd_num == 0
2399 && matches!(
2400 kind,
2401 ast::IoFileRedirectKind::Read | ast::IoFileRedirectKind::ReadAndWrite
2402 )
2403 {
2404 let filename = redirect_target_filename(target, state)?;
2405 let path = resolve_path(&state.cwd, &filename);
2406 if is_dev_stdin(&path) {
2407 return Ok(default_stdin.to_string());
2408 }
2409 if is_dev_null(&path) || is_dev_zero(&path) || is_dev_full(&path) {
2410 return Ok(String::new());
2411 }
2412 if filename.is_empty() {
2414 return Err(RustBashError::RedirectFailed(
2415 ": No such file or directory".to_string(),
2416 ));
2417 }
2418 let content = state.fs.read_file(Path::new(&path)).map_err(|_| {
2419 RustBashError::RedirectFailed(format!(
2420 "{filename}: No such file or directory"
2421 ))
2422 })?;
2423 return Ok(String::from_utf8_lossy(&content).to_string());
2424 }
2425 if fd_num == 0 && matches!(kind, ast::IoFileRedirectKind::DuplicateInput) {
2427 let dup_target = redirect_target_filename(target, state)?;
2428 if let Ok(source_fd) = dup_target.parse::<i32>()
2429 && let Some(pfd) = state.persistent_fds.get(&source_fd)
2430 {
2431 match pfd {
2432 PersistentFd::InputFile(path) | PersistentFd::ReadWriteFile(path) => {
2433 let content = state
2434 .fs
2435 .read_file(Path::new(path))
2436 .map_err(|e| RustBashError::Execution(e.to_string()))?;
2437 return Ok(String::from_utf8_lossy(&content).to_string());
2438 }
2439 PersistentFd::DevNull | PersistentFd::Closed => {
2440 return Ok(String::new());
2441 }
2442 PersistentFd::OutputFile(_) | PersistentFd::DupStdFd(_) => {}
2443 }
2444 }
2445 }
2446 }
2447 ast::IoRedirect::HereString(fd, word) => {
2448 let fd_num = fd.unwrap_or(0);
2449 if fd_num == 0 {
2450 let val = expand_word_to_string_mut(word, state)?;
2451 if val.len() > state.limits.max_heredoc_size {
2452 return Err(RustBashError::LimitExceeded {
2453 limit_name: "max_heredoc_size",
2454 limit_value: state.limits.max_heredoc_size,
2455 actual_value: val.len(),
2456 });
2457 }
2458 return Ok(format!("{val}\n"));
2459 }
2460 }
2461 ast::IoRedirect::HereDocument(fd, heredoc) => {
2462 let fd_num = fd.unwrap_or(0);
2463 if fd_num == 0 {
2464 let body = if heredoc.requires_expansion {
2465 expand_word_to_string_mut(&heredoc.doc, state)?
2466 } else {
2467 heredoc.doc.value.clone()
2468 };
2469 if body.len() > state.limits.max_heredoc_size {
2470 return Err(RustBashError::LimitExceeded {
2471 limit_name: "max_heredoc_size",
2472 limit_value: state.limits.max_heredoc_size,
2473 actual_value: body.len(),
2474 });
2475 }
2476 if heredoc.remove_tabs {
2477 return Ok(body
2478 .lines()
2479 .map(|l| l.trim_start_matches('\t'))
2480 .collect::<Vec<_>>()
2481 .join("\n")
2482 + if body.ends_with('\n') { "\n" } else { "" });
2483 }
2484 return Ok(body);
2485 }
2486 }
2487 _ => {}
2488 }
2489 }
2490 Ok(default_stdin.to_string())
2491}
2492
2493fn apply_output_redirects(
2494 redirects: &[&ast::IoRedirect],
2495 result: &mut ExecResult,
2496 state: &mut InterpreterState,
2497) -> Result<(), RustBashError> {
2498 let mut redirected_fds = std::collections::HashSet::new();
2500 let mut deferred_errors: Vec<String> = Vec::new();
2503
2504 for redir in redirects {
2505 match redir {
2506 ast::IoRedirect::File(fd, kind, target) => {
2507 let fd_num = match kind {
2508 ast::IoFileRedirectKind::Read
2509 | ast::IoFileRedirectKind::ReadAndWrite
2510 | ast::IoFileRedirectKind::DuplicateInput => fd.unwrap_or(0),
2511 _ => fd.unwrap_or(1),
2512 };
2513 redirected_fds.insert(fd_num);
2514 let cont =
2515 apply_file_redirect(*fd, kind, target, result, state, &mut deferred_errors)?;
2516 if !cont {
2517 break;
2518 }
2519 }
2520 ast::IoRedirect::OutputAndError(word, append) => {
2521 redirected_fds.insert(1);
2522 redirected_fds.insert(2);
2523 let filename = expand_word_to_string_mut(word, state)?;
2524 if filename.is_empty() {
2525 result
2526 .stderr
2527 .push_str("rust-bash: : No such file or directory\n");
2528 result.exit_code = 1;
2529 break;
2530 }
2531 let path = resolve_path(&state.cwd, &filename);
2532
2533 if state.shell_opts.noclobber
2535 && !*append
2536 && !is_dev_null(&path)
2537 && state.fs.exists(Path::new(&path))
2538 {
2539 result.stderr.push_str(&format!(
2540 "rust-bash: {filename}: cannot overwrite existing file\n"
2541 ));
2542 result.stdout.clear();
2543 result.exit_code = 1;
2544 break;
2545 }
2546
2547 let combined = format!("{}{}", result.stdout, result.stderr);
2548
2549 if is_dev_null(&path) {
2550 result.stdout.clear();
2551 result.stderr.clear();
2552 } else if *append {
2553 write_or_append(state, &path, &combined, true)?;
2554 result.stdout.clear();
2555 result.stderr.clear();
2556 } else {
2557 write_or_append(state, &path, &combined, false)?;
2558 result.stdout.clear();
2559 result.stderr.clear();
2560 }
2561 }
2562 _ => {} }
2564 }
2565
2566 apply_persistent_fd_fallback(result, state, &redirected_fds)?;
2568
2569 for err in deferred_errors {
2573 result.stderr.push_str(&err);
2574 }
2575
2576 Ok(())
2577}
2578
2579fn apply_persistent_fd_fallback(
2581 result: &mut ExecResult,
2582 state: &InterpreterState,
2583 redirected_fds: &std::collections::HashSet<i32>,
2584) -> Result<(), RustBashError> {
2585 if !redirected_fds.contains(&1)
2587 && let Some(pfd) = state.persistent_fds.get(&1)
2588 {
2589 match pfd {
2590 PersistentFd::OutputFile(path) => {
2591 if !result.stdout.is_empty() {
2592 write_or_append(state, path, &result.stdout, true)?;
2593 result.stdout.clear();
2594 }
2595 }
2596 PersistentFd::DevNull | PersistentFd::Closed => {
2597 result.stdout.clear();
2598 }
2599 _ => {}
2600 }
2601 }
2602
2603 if !redirected_fds.contains(&2)
2605 && let Some(pfd) = state.persistent_fds.get(&2)
2606 {
2607 match pfd {
2608 PersistentFd::OutputFile(path) => {
2609 if !result.stderr.is_empty() {
2610 write_or_append(state, path, &result.stderr, true)?;
2611 result.stderr.clear();
2612 }
2613 }
2614 PersistentFd::DevNull | PersistentFd::Closed => {
2615 result.stderr.clear();
2616 }
2617 _ => {}
2618 }
2619 }
2620
2621 Ok(())
2622}
2623
2624fn apply_file_redirect(
2627 fd: Option<i32>,
2628 kind: &ast::IoFileRedirectKind,
2629 target: &ast::IoFileRedirectTarget,
2630 result: &mut ExecResult,
2631 state: &mut InterpreterState,
2632 deferred_errors: &mut Vec<String>,
2633) -> Result<bool, RustBashError> {
2634 macro_rules! try_filename {
2636 ($target:expr, $state:expr, $result:expr) => {
2637 match redirect_target_filename($target, $state) {
2638 Ok(f) => f,
2639 Err(RustBashError::RedirectFailed(msg)) => {
2640 let fd_num = fd.unwrap_or(1);
2642 if fd_num == 1 {
2643 $result.stdout.clear();
2644 } else if fd_num == 2 {
2645 $result.stderr.clear();
2646 }
2647 $result.stderr.push_str(&format!("rust-bash: {msg}\n"));
2648 $result.exit_code = 1;
2649 return Ok(false);
2650 }
2651 Err(e) => return Err(e),
2652 }
2653 };
2654 }
2655
2656 match kind {
2657 ast::IoFileRedirectKind::Write | ast::IoFileRedirectKind::Clobber => {
2658 let fd_num = fd.unwrap_or(1);
2659 let filename = try_filename!(target, state, result);
2660 let path = resolve_path(&state.cwd, &filename);
2661
2662 if state.shell_opts.noclobber
2664 && matches!(kind, ast::IoFileRedirectKind::Write)
2665 && !is_dev_null(&path)
2666 && !is_special_dev_path(&path)
2667 && state.fs.exists(Path::new(&path))
2668 {
2669 result.stderr.push_str(&format!(
2670 "rust-bash: {filename}: cannot overwrite existing file\n"
2671 ));
2672 if fd_num == 1 {
2673 result.stdout.clear();
2674 }
2675 result.exit_code = 1;
2676 return Ok(false);
2677 }
2678
2679 apply_write_redirect(fd_num, &path, result, state, false, deferred_errors)?;
2680 }
2681 ast::IoFileRedirectKind::Append => {
2682 let fd_num = fd.unwrap_or(1);
2683 let filename = try_filename!(target, state, result);
2684 let path = resolve_path(&state.cwd, &filename);
2685 apply_write_redirect(fd_num, &path, result, state, true, deferred_errors)?;
2686 }
2687 ast::IoFileRedirectKind::DuplicateOutput => {
2688 let fd_num = fd.unwrap_or(1);
2689 if !apply_duplicate_output(fd_num, target, result, state)? {
2690 return Ok(false);
2691 }
2692 }
2693 ast::IoFileRedirectKind::DuplicateInput => {
2694 let fd_num = fd.unwrap_or(0);
2695 if fd_num == 0 {
2696 } else {
2698 if !apply_duplicate_output(fd_num, target, result, state)? {
2700 return Ok(false);
2701 }
2702 }
2703 }
2704 ast::IoFileRedirectKind::Read => {
2705 }
2707 ast::IoFileRedirectKind::ReadAndWrite => {
2708 let fd_num = fd.unwrap_or(0);
2709 let filename = try_filename!(target, state, result);
2710 let path = resolve_path(&state.cwd, &filename);
2711 if !state.fs.exists(Path::new(&path)) {
2712 state
2713 .fs
2714 .write_file(Path::new(&path), b"")
2715 .map_err(|e| RustBashError::Execution(e.to_string()))?;
2716 }
2717 if fd_num == 1 {
2720 write_or_append(state, &path, &result.stdout, false)?;
2721 result.stdout.clear();
2722 } else if fd_num == 2 {
2723 write_or_append(state, &path, &result.stderr, false)?;
2724 result.stderr.clear();
2725 }
2726 }
2727 }
2728 Ok(true)
2729}
2730
2731fn apply_write_redirect(
2733 fd_num: i32,
2734 path: &str,
2735 result: &mut ExecResult,
2736 state: &InterpreterState,
2737 append: bool,
2738 deferred_errors: &mut Vec<String>,
2739) -> Result<(), RustBashError> {
2740 if is_dev_null(path) || is_dev_zero(path) {
2741 if fd_num == 1 {
2742 result.stdout.clear();
2743 result.stdout_bytes = None;
2744 } else if fd_num == 2 {
2745 result.stderr.clear();
2746 }
2747 } else if is_dev_stdout(path) {
2748 if fd_num == 2 {
2750 result.stdout.push_str(&result.stderr);
2751 result.stderr.clear();
2752 }
2753 } else if is_dev_stderr(path) {
2754 if fd_num == 1 {
2756 result.stderr.push_str(&result.stdout);
2757 result.stdout.clear();
2758 }
2759 } else if is_dev_full(path) {
2760 deferred_errors
2764 .push("rust-bash: write error: /dev/full: No space left on device\n".to_string());
2765 if fd_num == 1 {
2766 result.stdout.clear();
2767 } else if fd_num == 2 {
2768 result.stderr.clear();
2769 }
2770 result.exit_code = 1;
2771 } else {
2772 let p = Path::new(path);
2774 if state.fs.exists(p)
2775 && let Ok(meta) = state.fs.stat(p)
2776 && meta.node_type == crate::vfs::NodeType::Directory
2777 {
2778 let basename = path.rsplit('/').next().unwrap_or(path);
2779 let display = if basename.is_empty() { path } else { basename };
2780 deferred_errors.push(format!("rust-bash: {display}: Is a directory\n"));
2781 if fd_num == 1 {
2782 result.stdout.clear();
2783 } else if fd_num == 2 {
2784 result.stderr.clear();
2785 }
2786 result.exit_code = 1;
2787 return Ok(());
2788 }
2789 let content_bytes: Vec<u8> = if fd_num == 1 {
2790 if let Some(bytes) = result.stdout_bytes.take() {
2792 bytes
2793 } else {
2794 result.stdout.as_bytes().to_vec()
2795 }
2796 } else if fd_num == 2 {
2797 result.stderr.as_bytes().to_vec()
2798 } else {
2799 return write_to_persistent_fd(fd_num, result, state);
2800 };
2801 write_or_append_bytes(state, path, &content_bytes, append)?;
2802 if fd_num == 1 {
2803 result.stdout.clear();
2804 result.stdout_bytes = None;
2805 } else if fd_num == 2 {
2806 result.stderr.clear();
2807 }
2808 }
2809 Ok(())
2810}
2811
2812fn write_to_persistent_fd(
2814 _fd_num: i32,
2815 _result: &mut ExecResult,
2816 _state: &InterpreterState,
2817) -> Result<(), RustBashError> {
2818 Ok(())
2820}
2821
2822fn apply_duplicate_output(
2825 fd_num: i32,
2826 target: &ast::IoFileRedirectTarget,
2827 result: &mut ExecResult,
2828 state: &mut InterpreterState,
2829) -> Result<bool, RustBashError> {
2830 let dup_target_str = match target {
2831 ast::IoFileRedirectTarget::Duplicate(word) => expand_word_to_string_mut(word, state)?,
2832 ast::IoFileRedirectTarget::Fd(target_fd) => target_fd.to_string(),
2833 _ => return Ok(true),
2834 };
2835
2836 if dup_target_str == "-" {
2838 if fd_num == 1 {
2839 result.stdout.clear();
2840 } else if fd_num == 2 {
2841 result.stderr.clear();
2842 }
2843 return Ok(true);
2844 }
2845
2846 if let Some(source_str) = dup_target_str.strip_suffix('-') {
2848 if let Ok(source_fd) = source_str.parse::<i32>() {
2849 apply_dup_fd(fd_num, source_fd, result, state)?;
2851 if source_fd == 1 {
2853 result.stdout.clear();
2854 } else if source_fd == 2 {
2855 result.stderr.clear();
2856 } else {
2857 state.persistent_fds.insert(source_fd, PersistentFd::Closed);
2858 }
2859 }
2860 return Ok(true);
2861 }
2862
2863 if let Ok(target_fd) = dup_target_str.parse::<i32>() {
2865 if target_fd != 0
2867 && target_fd != 1
2868 && target_fd != 2
2869 && !state.persistent_fds.contains_key(&target_fd)
2870 {
2871 if fd_num == 1 {
2872 result.stdout.clear();
2873 }
2874 result
2875 .stderr
2876 .push_str(&format!("rust-bash: {fd_num}: Bad file descriptor\n"));
2877 result.exit_code = 1;
2878 return Ok(false);
2879 }
2880 apply_dup_fd(fd_num, target_fd, result, state)?;
2881 }
2882 Ok(true)
2883}
2884
2885fn apply_dup_fd(
2887 fd_num: i32,
2888 target_fd: i32,
2889 result: &mut ExecResult,
2890 state: &InterpreterState,
2891) -> Result<(), RustBashError> {
2892 if target_fd == 1 && fd_num == 2 {
2894 result.stdout.push_str(&result.stderr);
2896 result.stderr.clear();
2897 } else if target_fd == 2 && fd_num == 1 {
2898 result.stderr.push_str(&result.stdout);
2900 result.stdout.clear();
2901 } else if fd_num == 1 || fd_num == 2 {
2902 if let Some(pfd) = state.persistent_fds.get(&target_fd) {
2904 match pfd {
2905 PersistentFd::OutputFile(path) => {
2906 let content = if fd_num == 1 {
2907 let c = result.stdout.clone();
2908 result.stdout.clear();
2909 c
2910 } else {
2911 let c = result.stderr.clone();
2912 result.stderr.clear();
2913 c
2914 };
2915 write_or_append(state, path, &content, true)?;
2916 }
2917 PersistentFd::DevNull | PersistentFd::Closed => {
2918 if fd_num == 1 {
2919 result.stdout.clear();
2920 } else {
2921 result.stderr.clear();
2922 }
2923 }
2924 PersistentFd::DupStdFd(std_fd) => {
2925 if *std_fd == 1 && fd_num == 2 {
2927 result.stdout.push_str(&result.stderr);
2928 result.stderr.clear();
2929 } else if *std_fd == 2 && fd_num == 1 {
2930 result.stderr.push_str(&result.stdout);
2931 result.stdout.clear();
2932 }
2933 }
2935 _ => {}
2936 }
2937 }
2938 }
2939 Ok(())
2940}
2941
2942fn expand_process_substitution<'a>(
2947 kind: &ast::ProcessSubstitutionKind,
2948 list: &'a ast::CompoundList,
2949 state: &mut InterpreterState,
2950 deferred_write_subs: &mut Vec<(&'a ast::CompoundList, String)>,
2951) -> Result<String, RustBashError> {
2952 match kind {
2953 ast::ProcessSubstitutionKind::Read => execute_read_process_substitution(list, state),
2954 ast::ProcessSubstitutionKind::Write => {
2955 let path = allocate_proc_sub_temp_file(state, b"")?;
2956 deferred_write_subs.push((list, path.clone()));
2957 Ok(path)
2958 }
2959 }
2960}
2961
2962fn redirect_target_filename(
2963 target: &ast::IoFileRedirectTarget,
2964 state: &mut InterpreterState,
2965) -> Result<String, RustBashError> {
2966 match target {
2967 ast::IoFileRedirectTarget::Filename(word) => {
2968 let filename = expand_word_to_string_mut(word, state)?;
2969 if filename.is_empty() {
2970 return Err(RustBashError::RedirectFailed(
2971 ": No such file or directory".to_string(),
2972 ));
2973 }
2974 Ok(filename)
2975 }
2976 ast::IoFileRedirectTarget::Fd(fd) => Ok(fd.to_string()),
2977 ast::IoFileRedirectTarget::Duplicate(word) => expand_word_to_string_mut(word, state),
2978 ast::IoFileRedirectTarget::ProcessSubstitution(_, _) => {
2981 let key = std::ptr::from_ref(target) as usize;
2983 state.proc_sub_prealloc.remove(&key).ok_or_else(|| {
2984 RustBashError::Execution(
2985 "process substitution: no pre-allocated path available".into(),
2986 )
2987 })
2988 }
2989 }
2990}
2991
2992fn execute_read_process_substitution(
2995 list: &ast::CompoundList,
2996 state: &mut InterpreterState,
2997) -> Result<String, RustBashError> {
2998 let mut sub_state = make_proc_sub_state(state);
2999 let result = execute_compound_list(list, &mut sub_state, "")?;
3000
3001 state.counters.command_count = sub_state.counters.command_count;
3003 state.counters.output_size = sub_state.counters.output_size;
3004 state.proc_sub_counter = sub_state.proc_sub_counter;
3005
3006 allocate_proc_sub_temp_file(state, result.stdout.as_bytes())
3007}
3008
3009fn allocate_proc_sub_temp_file(
3011 state: &mut InterpreterState,
3012 content: &[u8],
3013) -> Result<String, RustBashError> {
3014 let path = format!("/tmp/.proc_sub_{}", state.proc_sub_counter);
3015 state.proc_sub_counter += 1;
3016
3017 let tmp = Path::new("/tmp");
3019 if !state.fs.exists(tmp) {
3020 state
3021 .fs
3022 .mkdir_p(tmp)
3023 .map_err(|e| RustBashError::Execution(e.to_string()))?;
3024 }
3025
3026 state
3027 .fs
3028 .write_file(Path::new(&path), content)
3029 .map_err(|e| RustBashError::Execution(e.to_string()))?;
3030
3031 Ok(path)
3032}
3033
3034fn make_proc_sub_state(state: &mut InterpreterState) -> InterpreterState {
3038 InterpreterState {
3039 fs: Arc::clone(&state.fs),
3040 env: state.env.clone(),
3041 cwd: state.cwd.clone(),
3042 functions: state.functions.clone(),
3043 last_exit_code: state.last_exit_code,
3044 commands: clone_commands(&state.commands),
3045 shell_opts: state.shell_opts.clone(),
3046 shopt_opts: state.shopt_opts.clone(),
3047 limits: state.limits.clone(),
3048 counters: ExecutionCounters {
3049 command_count: state.counters.command_count,
3050 output_size: state.counters.output_size,
3051 start_time: state.counters.start_time,
3052 substitution_depth: state.counters.substitution_depth,
3053 call_depth: 0,
3054 },
3055 network_policy: state.network_policy.clone(),
3056 should_exit: false,
3057 loop_depth: 0,
3058 control_flow: None,
3059 positional_params: state.positional_params.clone(),
3060 shell_name: state.shell_name.clone(),
3061 random_seed: state.random_seed,
3062 local_scopes: Vec::new(),
3063 in_function_depth: 0,
3064 traps: HashMap::new(),
3065 in_trap: false,
3066 errexit_suppressed: 0,
3067 stdin_offset: 0,
3068 dir_stack: state.dir_stack.clone(),
3069 command_hash: state.command_hash.clone(),
3070 aliases: state.aliases.clone(),
3071 current_lineno: state.current_lineno,
3072 shell_start_time: state.shell_start_time,
3073 last_argument: state.last_argument.clone(),
3074 call_stack: state.call_stack.clone(),
3075 machtype: state.machtype.clone(),
3076 hosttype: state.hosttype.clone(),
3077 persistent_fds: HashMap::new(),
3078 next_auto_fd: 10,
3079 proc_sub_counter: state.proc_sub_counter,
3080 proc_sub_prealloc: HashMap::new(),
3081 pipe_stdin_bytes: None,
3082 }
3083}
3084
3085fn is_dev_null(path: &str) -> bool {
3086 path == "/dev/null"
3087}
3088
3089fn write_or_append(
3090 state: &InterpreterState,
3091 path: &str,
3092 content: &str,
3093 append: bool,
3094) -> Result<(), RustBashError> {
3095 write_or_append_bytes(state, path, content.as_bytes(), append)
3096}
3097
3098fn write_or_append_bytes(
3099 state: &InterpreterState,
3100 path: &str,
3101 content: &[u8],
3102 append: bool,
3103) -> Result<(), RustBashError> {
3104 let p = Path::new(path);
3105
3106 if append {
3107 if state.fs.exists(p) {
3108 state
3109 .fs
3110 .append_file(p, content)
3111 .map_err(|e| RustBashError::Execution(e.to_string()))?;
3112 } else {
3113 state
3114 .fs
3115 .write_file(p, content)
3116 .map_err(|e| RustBashError::Execution(e.to_string()))?;
3117 }
3118 } else {
3119 state
3120 .fs
3121 .write_file(p, content)
3122 .map_err(|e| RustBashError::Execution(e.to_string()))?;
3123 }
3124 Ok(())
3125}
3126
3127fn execute_extended_test(
3130 expr: &ast::ExtendedTestExpr,
3131 state: &mut InterpreterState,
3132) -> Result<ExecResult, RustBashError> {
3133 let should_trace = state.shell_opts.xtrace;
3134 let mut exec_result = match eval_extended_test_expr(expr, state) {
3135 Ok(result) => ExecResult {
3136 exit_code: if result { 0 } else { 1 },
3137 ..ExecResult::default()
3138 },
3139 Err(RustBashError::Execution(ref msg)) => {
3140 let exit_code = if msg.contains("invalid regex") { 2 } else { 1 };
3141 state.last_exit_code = exit_code;
3142 ExecResult {
3143 stderr: format!("rust-bash: {msg}\n"),
3144 exit_code,
3145 ..ExecResult::default()
3146 }
3147 }
3148 Err(e) => return Err(e),
3149 };
3150 if should_trace {
3151 let repr = format_extended_test_expr_expanded(expr, state);
3152 let ps4 = expand_ps4(state);
3153 exec_result.stderr = format!("{ps4}[[ {repr} ]]\n{}", exec_result.stderr);
3154 }
3155 Ok(exec_result)
3156}
3157
3158fn format_extended_test_expr_expanded(
3160 expr: &ast::ExtendedTestExpr,
3161 state: &mut InterpreterState,
3162) -> String {
3163 match expr {
3164 ast::ExtendedTestExpr::And(l, r) => {
3165 format!(
3166 "{} && {}",
3167 format_extended_test_expr_expanded(l, state),
3168 format_extended_test_expr_expanded(r, state)
3169 )
3170 }
3171 ast::ExtendedTestExpr::Or(l, r) => {
3172 format!(
3173 "{} || {}",
3174 format_extended_test_expr_expanded(l, state),
3175 format_extended_test_expr_expanded(r, state)
3176 )
3177 }
3178 ast::ExtendedTestExpr::Not(inner) => {
3179 format!("! {}", format_extended_test_expr_expanded(inner, state))
3180 }
3181 ast::ExtendedTestExpr::Parenthesized(inner) => {
3182 format_extended_test_expr_expanded(inner, state)
3183 }
3184 ast::ExtendedTestExpr::UnaryTest(pred, word) => {
3185 let expanded = expand_word_to_string_mut(word, state).unwrap_or_default();
3186 format!("{} {}", format_unary_pred(pred), expanded)
3187 }
3188 ast::ExtendedTestExpr::BinaryTest(pred, l, r) => {
3189 let l_exp = expand_word_to_string_mut(l, state).unwrap_or_default();
3190 let r_exp = expand_word_to_string_mut(r, state).unwrap_or_default();
3191 format!("{} {} {}", l_exp, format_binary_pred(pred), r_exp)
3192 }
3193 }
3194}
3195
3196fn format_unary_pred(pred: &ast::UnaryPredicate) -> &'static str {
3197 use brush_parser::ast::UnaryPredicate;
3198 match pred {
3199 UnaryPredicate::FileExists => "-a",
3200 UnaryPredicate::FileExistsAndIsBlockSpecialFile => "-b",
3201 UnaryPredicate::FileExistsAndIsCharSpecialFile => "-c",
3202 UnaryPredicate::FileExistsAndIsDir => "-d",
3203 UnaryPredicate::FileExistsAndIsRegularFile => "-f",
3204 UnaryPredicate::FileExistsAndIsSetgid => "-g",
3205 UnaryPredicate::FileExistsAndIsSymlink => "-h",
3206 UnaryPredicate::FileExistsAndHasStickyBit => "-k",
3207 UnaryPredicate::FileExistsAndIsFifo => "-p",
3208 UnaryPredicate::FileExistsAndIsReadable => "-r",
3209 UnaryPredicate::FileExistsAndIsNotZeroLength => "-s",
3210 UnaryPredicate::FdIsOpenTerminal => "-t",
3211 UnaryPredicate::FileExistsAndIsSetuid => "-u",
3212 UnaryPredicate::FileExistsAndIsWritable => "-w",
3213 UnaryPredicate::FileExistsAndIsExecutable => "-x",
3214 UnaryPredicate::FileExistsAndOwnedByEffectiveGroupId => "-G",
3215 UnaryPredicate::FileExistsAndModifiedSinceLastRead => "-N",
3216 UnaryPredicate::FileExistsAndOwnedByEffectiveUserId => "-O",
3217 UnaryPredicate::FileExistsAndIsSocket => "-S",
3218 UnaryPredicate::StringHasZeroLength => "-z",
3219 UnaryPredicate::StringHasNonZeroLength => "-n",
3220 UnaryPredicate::ShellOptionEnabled => "-o",
3221 UnaryPredicate::ShellVariableIsSetAndAssigned => "-v",
3222 UnaryPredicate::ShellVariableIsSetAndNameRef => "-R",
3223 }
3224}
3225
3226fn format_binary_pred(pred: &ast::BinaryPredicate) -> &'static str {
3227 use brush_parser::ast::BinaryPredicate;
3228 match pred {
3229 BinaryPredicate::StringExactlyMatchesPattern => "==",
3230 BinaryPredicate::StringDoesNotExactlyMatchPattern => "!=",
3231 BinaryPredicate::StringExactlyMatchesString => "==",
3232 BinaryPredicate::StringDoesNotExactlyMatchString => "!=",
3233 BinaryPredicate::StringMatchesRegex => "=~",
3234 BinaryPredicate::StringContainsSubstring => "=~",
3235 BinaryPredicate::ArithmeticEqualTo => "-eq",
3236 BinaryPredicate::ArithmeticNotEqualTo => "-ne",
3237 BinaryPredicate::ArithmeticLessThan => "-lt",
3238 BinaryPredicate::ArithmeticGreaterThan => "-gt",
3239 BinaryPredicate::ArithmeticLessThanOrEqualTo => "-le",
3240 BinaryPredicate::ArithmeticGreaterThanOrEqualTo => "-ge",
3241 BinaryPredicate::FilesReferToSameDeviceAndInodeNumbers => "-ef",
3242 BinaryPredicate::LeftFileIsNewerOrExistsWhenRightDoesNot => "-nt",
3243 BinaryPredicate::LeftFileIsOlderOrDoesNotExistWhenRightDoes => "-ot",
3244 _ => "?",
3245 }
3246}
3247
3248fn eval_extended_test_expr(
3249 expr: &ast::ExtendedTestExpr,
3250 state: &mut InterpreterState,
3251) -> Result<bool, RustBashError> {
3252 match expr {
3253 ast::ExtendedTestExpr::And(left, right) => {
3254 let l = eval_extended_test_expr(left, state)?;
3255 if !l {
3256 return Ok(false);
3257 }
3258 eval_extended_test_expr(right, state)
3259 }
3260 ast::ExtendedTestExpr::Or(left, right) => {
3261 let l = eval_extended_test_expr(left, state)?;
3262 if l {
3263 return Ok(true);
3264 }
3265 eval_extended_test_expr(right, state)
3266 }
3267 ast::ExtendedTestExpr::Not(inner) => {
3268 let val = eval_extended_test_expr(inner, state)?;
3269 Ok(!val)
3270 }
3271 ast::ExtendedTestExpr::Parenthesized(inner) => eval_extended_test_expr(inner, state),
3272 ast::ExtendedTestExpr::UnaryTest(pred, word) => {
3273 use brush_parser::ast::UnaryPredicate;
3274 if matches!(pred, UnaryPredicate::ShellVariableIsSetAndAssigned) {
3276 let operand = expand_word_to_string_mut(word, state)?;
3277 return Ok(test_variable_is_set(&operand, state));
3278 }
3279 let operand = expand_word_to_string_mut(word, state)?;
3280 let env: HashMap<String, String> = state
3281 .env
3282 .iter()
3283 .map(|(k, v)| (k.clone(), v.value.as_scalar().to_string()))
3284 .collect();
3285 Ok(crate::commands::test_cmd::eval_unary_predicate(
3286 pred,
3287 &operand,
3288 &*state.fs,
3289 &state.cwd,
3290 &env,
3291 Some(&state.shell_opts),
3292 ))
3293 }
3294 ast::ExtendedTestExpr::BinaryTest(pred, left_word, right_word) => {
3295 let left = expand_word_to_string_mut(left_word, state)?;
3296
3297 if matches!(
3299 pred,
3300 ast::BinaryPredicate::StringMatchesRegex
3301 | ast::BinaryPredicate::StringContainsSubstring
3302 ) {
3303 let raw = &right_word.value;
3306 let is_fully_quoted = is_word_fully_quoted(raw);
3307 let pattern = expand_word_to_string_mut(right_word, state)?;
3308 if is_fully_quoted {
3309 return Ok(left.contains(&pattern));
3310 }
3311 let effective_pattern = build_regex_with_quoted_literals(raw, state)?;
3313 return eval_regex_match(&left, &effective_pattern, state);
3314 }
3315
3316 let right = expand_word_to_string_mut(right_word, state)?;
3317
3318 use brush_parser::ast::BinaryPredicate;
3323 if matches!(
3324 pred,
3325 BinaryPredicate::ArithmeticEqualTo
3326 | BinaryPredicate::ArithmeticNotEqualTo
3327 | BinaryPredicate::ArithmeticLessThan
3328 | BinaryPredicate::ArithmeticGreaterThan
3329 | BinaryPredicate::ArithmeticLessThanOrEqualTo
3330 | BinaryPredicate::ArithmeticGreaterThanOrEqualTo
3331 ) {
3332 let lval =
3333 crate::commands::test_cmd::parse_bash_int_pub(&left).unwrap_or_else(|| {
3334 crate::interpreter::arithmetic::eval_arithmetic(&left, state).unwrap_or(0)
3335 });
3336 let rval =
3337 crate::commands::test_cmd::parse_bash_int_pub(&right).unwrap_or_else(|| {
3338 crate::interpreter::arithmetic::eval_arithmetic(&right, state).unwrap_or(0)
3339 });
3340 let result = match pred {
3341 BinaryPredicate::ArithmeticEqualTo => lval == rval,
3342 BinaryPredicate::ArithmeticNotEqualTo => lval != rval,
3343 BinaryPredicate::ArithmeticLessThan => lval < rval,
3344 BinaryPredicate::ArithmeticGreaterThan => lval > rval,
3345 BinaryPredicate::ArithmeticLessThanOrEqualTo => lval <= rval,
3346 BinaryPredicate::ArithmeticGreaterThanOrEqualTo => lval >= rval,
3347 _ => unreachable!(),
3348 };
3349 return Ok(result);
3350 }
3351
3352 if state.shopt_opts.nocasematch {
3355 let result = match pred {
3357 ast::BinaryPredicate::StringExactlyMatchesPattern => {
3358 crate::interpreter::pattern::extglob_match_nocase(&right, &left)
3359 }
3360 ast::BinaryPredicate::StringDoesNotExactlyMatchPattern => {
3361 !crate::interpreter::pattern::extglob_match_nocase(&right, &left)
3362 }
3363 ast::BinaryPredicate::StringExactlyMatchesString => {
3364 left.eq_ignore_ascii_case(&right)
3365 }
3366 ast::BinaryPredicate::StringDoesNotExactlyMatchString => {
3367 !left.eq_ignore_ascii_case(&right)
3368 }
3369 _ => crate::commands::test_cmd::eval_binary_predicate(
3370 pred, &left, &right, true, &*state.fs, &state.cwd,
3371 ),
3372 };
3373 Ok(result)
3374 } else {
3375 let result = match pred {
3377 ast::BinaryPredicate::StringExactlyMatchesPattern => {
3378 crate::interpreter::pattern::extglob_match(&right, &left)
3379 }
3380 ast::BinaryPredicate::StringDoesNotExactlyMatchPattern => {
3381 !crate::interpreter::pattern::extglob_match(&right, &left)
3382 }
3383 _ => crate::commands::test_cmd::eval_binary_predicate(
3384 pred, &left, &right, true, &*state.fs, &state.cwd,
3385 ),
3386 };
3387 Ok(result)
3388 }
3389 }
3390 }
3391}
3392
3393fn test_variable_is_set(operand: &str, state: &mut InterpreterState) -> bool {
3396 if let Some(bracket_pos) = operand.find('[')
3398 && operand.ends_with(']')
3399 {
3400 let name = &operand[..bracket_pos];
3401 let index = &operand[bracket_pos + 1..operand.len() - 1];
3402 let resolved = crate::interpreter::resolve_nameref_or_self(name, state);
3403
3404 if index == "@" || index == "*" {
3405 return state
3406 .env
3407 .get(&resolved)
3408 .is_some_and(|var| match &var.value {
3409 VariableValue::IndexedArray(map) => !map.is_empty(),
3410 VariableValue::AssociativeArray(map) => !map.is_empty(),
3411 _ => false,
3412 });
3413 }
3414
3415 let var_type = state.env.get(&resolved).map(|var| match &var.value {
3417 VariableValue::IndexedArray(_) => 0,
3418 VariableValue::AssociativeArray(_) => 1,
3419 VariableValue::Scalar(_) => 2,
3420 });
3421
3422 return match var_type {
3423 Some(0) => {
3424 let idx = eval_index_arithmetic(index, state);
3426 let Some(var) = state.env.get(&resolved) else {
3427 return false;
3428 };
3429 if let VariableValue::IndexedArray(map) = &var.value {
3430 let actual_idx = if idx < 0 {
3431 let max_key = map.keys().next_back().copied().unwrap_or(0);
3432 let resolved_idx = max_key as i64 + 1 + idx;
3433 if resolved_idx < 0 {
3434 return false;
3435 }
3436 resolved_idx as usize
3437 } else {
3438 idx as usize
3439 };
3440 map.contains_key(&actual_idx)
3441 } else {
3442 false
3443 }
3444 }
3445 Some(1) => {
3446 state
3448 .env
3449 .get(&resolved)
3450 .and_then(|var| {
3451 if let VariableValue::AssociativeArray(map) = &var.value {
3452 Some(map.contains_key(index))
3453 } else {
3454 None
3455 }
3456 })
3457 .unwrap_or(false)
3458 }
3459 Some(2) => {
3460 let idx = eval_index_arithmetic(index, state);
3462 idx == 0 || idx == -1
3463 }
3464 _ => false,
3465 };
3466 }
3467 let resolved = crate::interpreter::resolve_nameref_or_self(operand, state);
3469 state.env.contains_key(&resolved)
3470}
3471
3472fn eval_index_arithmetic(index: &str, state: &mut InterpreterState) -> i64 {
3475 crate::interpreter::arithmetic::eval_arithmetic(index, state)
3476 .unwrap_or_else(|_| crate::interpreter::expansion::simple_arith_eval(index, state))
3477}
3478
3479fn eval_regex_match(
3480 string: &str,
3481 pattern: &str,
3482 state: &mut InterpreterState,
3483) -> Result<bool, RustBashError> {
3484 let effective_pattern = if state.shopt_opts.nocasematch {
3486 format!("(?i){pattern}")
3487 } else {
3488 pattern.to_string()
3489 };
3490 let re = regex::Regex::new(&effective_pattern)
3491 .map_err(|e| RustBashError::Execution(format!("invalid regex '{pattern}': {e}")))?;
3492
3493 if let Some(captures) = re.captures(string) {
3494 let mut map = std::collections::BTreeMap::new();
3497 let whole = captures.get(0).map(|m| m.as_str()).unwrap_or("");
3498 map.insert(0, whole.to_string());
3499 for i in 1..captures.len() {
3500 let val = captures.get(i).map(|m| m.as_str()).unwrap_or("");
3501 map.insert(i, val.to_string());
3502 }
3503 state.env.insert(
3504 "BASH_REMATCH".to_string(),
3505 Variable {
3506 value: VariableValue::IndexedArray(map),
3507 attrs: VariableAttrs::empty(),
3508 },
3509 );
3510 Ok(true)
3511 } else {
3512 state.env.insert(
3514 "BASH_REMATCH".to_string(),
3515 Variable {
3516 value: VariableValue::IndexedArray(std::collections::BTreeMap::new()),
3517 attrs: VariableAttrs::empty(),
3518 },
3519 );
3520 Ok(false)
3521 }
3522}
3523
3524fn is_word_fully_quoted(raw: &str) -> bool {
3526 let trimmed = raw.trim();
3527 if trimmed.len() < 2 {
3528 return false;
3529 }
3530 if trimmed.starts_with('\'') && trimmed.ends_with('\'') {
3532 return true;
3533 }
3534 if trimmed.starts_with('"') && trimmed.ends_with('"') {
3536 return true;
3537 }
3538 if (trimmed.starts_with("$'") && trimmed.ends_with('\''))
3540 || (trimmed.starts_with("$\"") && trimmed.ends_with('"'))
3541 {
3542 return true;
3543 }
3544 false
3545}
3546
3547fn build_regex_with_quoted_literals(
3550 raw: &str,
3551 state: &mut InterpreterState,
3552) -> Result<String, RustBashError> {
3553 let mut result = String::new();
3554 let chars: Vec<char> = raw.chars().collect();
3555 let mut i = 0;
3556 while i < chars.len() {
3557 match chars[i] {
3558 '\'' => {
3559 i += 1;
3561 let mut literal = String::new();
3562 while i < chars.len() && chars[i] != '\'' {
3563 literal.push(chars[i]);
3564 i += 1;
3565 }
3566 if i < chars.len() {
3567 i += 1; }
3569 result.push_str(®ex::escape(&literal));
3570 }
3571 '"' => {
3572 i += 1;
3574 let mut content = String::new();
3575 while i < chars.len() && chars[i] != '"' {
3576 if chars[i] == '\\' && i + 1 < chars.len() {
3577 content.push(chars[i + 1]);
3578 i += 2;
3579 } else {
3580 content.push(chars[i]);
3581 i += 1;
3582 }
3583 }
3584 if i < chars.len() {
3585 i += 1; }
3587 let word = ast::Word {
3589 value: content,
3590 loc: None,
3591 };
3592 let expanded = expand_word_to_string_mut(&word, state)?;
3593 result.push_str(®ex::escape(&expanded));
3594 }
3595 '\\' if i + 1 < chars.len() => {
3596 result.push_str(®ex::escape(&chars[i + 1].to_string()));
3598 i += 2;
3599 }
3600 '$' => {
3601 let mut var_text = String::new();
3603 var_text.push('$');
3604 i += 1;
3605 if i < chars.len() && chars[i] == '{' {
3606 var_text.push('{');
3608 i += 1;
3609 let mut depth = 1;
3610 while i < chars.len() && depth > 0 {
3611 if chars[i] == '{' {
3612 depth += 1;
3613 } else if chars[i] == '}' {
3614 depth -= 1;
3615 }
3616 var_text.push(chars[i]);
3617 i += 1;
3618 }
3619 } else {
3620 while i < chars.len() && (chars[i].is_ascii_alphanumeric() || chars[i] == '_') {
3622 var_text.push(chars[i]);
3623 i += 1;
3624 }
3625 }
3626 let word = ast::Word {
3627 value: var_text,
3628 loc: None,
3629 };
3630 let expanded = expand_word_to_string_mut(&word, state)?;
3631 result.push_str(&expanded);
3632 }
3633 c => {
3634 result.push(c);
3635 i += 1;
3636 }
3637 }
3638 }
3639 Ok(result)
3640}