1use std::cell::RefCell;
2use std::fs::{File, OpenOptions};
3use std::io::{BufRead, BufReader, Write, pipe};
4use std::os::fd::{AsRawFd, FromRawFd, RawFd};
5use std::os::unix::process::CommandExt;
6use std::process::{Command, Stdio};
7use std::rc::Rc;
8
9use super::parser::{Ast, Redirection, ShellCommand};
10use super::state::ShellState;
11
12const MAX_SUBSHELL_DEPTH: usize = 100;
14
15fn execute_and_capture_output(ast: Ast, shell_state: &mut ShellState) -> Result<String, String> {
18 let (reader, writer) = pipe().map_err(|e| format!("Failed to create pipe: {}", e))?;
20
21 match &ast {
26 Ast::Pipeline(commands) => {
27 if commands.is_empty() {
29 return Ok(String::new());
30 }
31
32 if commands.len() == 1 {
33 let cmd = &commands[0];
35 if cmd.args.is_empty() {
36 return Ok(String::new());
37 }
38
39 let var_expanded_args = expand_variables_in_args(&cmd.args, shell_state);
41 let expanded_args = expand_wildcards(&var_expanded_args)
42 .map_err(|e| format!("Wildcard expansion failed: {}", e))?;
43
44 if expanded_args.is_empty() {
45 return Ok(String::new());
46 }
47
48 if shell_state.get_function(&expanded_args[0]).is_some() {
50 let previous_capture = shell_state.capture_output.clone();
52
53 let capture_buffer = Rc::new(RefCell::new(Vec::new()));
55 shell_state.capture_output = Some(capture_buffer.clone());
56
57 let function_call_ast = Ast::FunctionCall {
59 name: expanded_args[0].clone(),
60 args: expanded_args[1..].to_vec(),
61 };
62
63 let exit_code = execute(function_call_ast, shell_state);
64
65 let captured = capture_buffer.borrow().clone();
67 let output = String::from_utf8_lossy(&captured).trim_end().to_string();
68
69 shell_state.capture_output = previous_capture;
71
72 if exit_code == 0 {
73 Ok(output)
74 } else {
75 Err(format!("Function failed with exit code {}", exit_code))
76 }
77 } else if crate::builtins::is_builtin(&expanded_args[0]) {
78 let temp_cmd = ShellCommand {
79 args: expanded_args,
80 redirections: cmd.redirections.clone(),
81 compound: None,
82 };
83
84 let exit_code = crate::builtins::execute_builtin(
86 &temp_cmd,
87 shell_state,
88 Some(Box::new(writer)),
89 );
90
91 drop(temp_cmd); let mut output = String::new();
94 use std::io::Read;
95 let mut reader = reader;
96 reader
97 .read_to_string(&mut output)
98 .map_err(|e| format!("Failed to read output: {}", e))?;
99
100 if exit_code == 0 {
101 Ok(output.trim_end().to_string())
102 } else {
103 Err(format!("Command failed with exit code {}", exit_code))
104 }
105 } else {
106 drop(writer); let mut command = Command::new(&expanded_args[0]);
110 command.args(&expanded_args[1..]);
111 command.stdout(Stdio::piped());
112 command.stderr(Stdio::null()); let child_env = shell_state.get_env_for_child();
116 command.env_clear();
117 for (key, value) in child_env {
118 command.env(key, value);
119 }
120
121 let output = command
122 .output()
123 .map_err(|e| format!("Failed to execute command: {}", e))?;
124
125 if output.status.success() {
126 Ok(String::from_utf8_lossy(&output.stdout)
127 .trim_end()
128 .to_string())
129 } else {
130 Err(format!(
131 "Command failed with exit code {}",
132 output.status.code().unwrap_or(1)
133 ))
134 }
135 }
136 } else {
137 drop(writer); let previous_capture = shell_state.capture_output.clone();
142
143 let capture_buffer = Rc::new(RefCell::new(Vec::new()));
145 shell_state.capture_output = Some(capture_buffer.clone());
146
147 let exit_code = execute_pipeline(commands, shell_state);
149
150 let captured = capture_buffer.borrow().clone();
152 let output = String::from_utf8_lossy(&captured).trim_end().to_string();
153
154 shell_state.capture_output = previous_capture;
156
157 if exit_code == 0 {
158 Ok(output)
159 } else {
160 Err(format!("Pipeline failed with exit code {}", exit_code))
161 }
162 }
163 }
164 _ => {
165 drop(writer);
167
168 let previous_capture = shell_state.capture_output.clone();
170
171 let capture_buffer = Rc::new(RefCell::new(Vec::new()));
173 shell_state.capture_output = Some(capture_buffer.clone());
174
175 let exit_code = execute(ast, shell_state);
177
178 let captured = capture_buffer.borrow().clone();
180 let output = String::from_utf8_lossy(&captured).trim_end().to_string();
181
182 shell_state.capture_output = previous_capture;
184
185 if exit_code == 0 {
186 Ok(output)
187 } else {
188 Err(format!("Command failed with exit code {}", exit_code))
189 }
190 }
191 }
192}
193
194fn expand_variables_in_args(args: &[String], shell_state: &mut ShellState) -> Vec<String> {
195 let mut expanded_args = Vec::new();
196
197 for arg in args {
198 let expanded_arg = expand_variables_in_string(arg, shell_state);
200 expanded_args.push(expanded_arg);
201 }
202
203 expanded_args
204}
205
206pub fn expand_variables_in_string(input: &str, shell_state: &mut ShellState) -> String {
207 let mut result = String::new();
208 let mut chars = input.chars().peekable();
209
210 while let Some(ch) = chars.next() {
211 if ch == '$' {
212 if let Some(&'(') = chars.peek() {
214 chars.next(); if let Some(&'(') = chars.peek() {
218 chars.next(); let mut arithmetic_expr = String::new();
221 let mut paren_depth = 1;
222 let mut found_closing = false;
223
224 while let Some(c) = chars.next() {
225 if c == '(' {
226 paren_depth += 1;
227 arithmetic_expr.push(c);
228 } else if c == ')' {
229 paren_depth -= 1;
230 if paren_depth == 0 {
231 if let Some(&')') = chars.peek() {
233 chars.next(); found_closing = true;
235 break;
236 } else {
237 result.push_str("$((");
239 result.push_str(&arithmetic_expr);
240 result.push(')');
241 break;
242 }
243 }
244 arithmetic_expr.push(c);
245 } else {
246 arithmetic_expr.push(c);
247 }
248 }
249
250 if found_closing {
251 let mut expanded_expr = String::new();
255 let mut expr_chars = arithmetic_expr.chars().peekable();
256
257 while let Some(ch) = expr_chars.next() {
258 if ch == '$' {
259 let mut var_name = String::new();
261 if let Some(&c) = expr_chars.peek() {
262 if c == '?'
263 || c == '$'
264 || c == '0'
265 || c == '#'
266 || c == '*'
267 || c == '@'
268 || c.is_ascii_digit()
269 {
270 var_name.push(c);
271 expr_chars.next();
272 } else {
273 while let Some(&c) = expr_chars.peek() {
274 if c.is_alphanumeric() || c == '_' {
275 var_name.push(c);
276 expr_chars.next();
277 } else {
278 break;
279 }
280 }
281 }
282 }
283
284 if !var_name.is_empty() {
285 if let Some(value) = shell_state.get_var(&var_name) {
286 expanded_expr.push_str(&value);
287 } else {
288 expanded_expr.push('0');
290 }
291 } else {
292 expanded_expr.push('$');
293 }
294 } else {
295 expanded_expr.push(ch);
296 }
297 }
298
299 match crate::arithmetic::evaluate_arithmetic_expression(
300 &expanded_expr,
301 shell_state,
302 ) {
303 Ok(value) => {
304 result.push_str(&value.to_string());
305 }
306 Err(e) => {
307 if shell_state.colors_enabled {
309 result.push_str(&format!(
310 "{}arithmetic error: {}{}",
311 shell_state.color_scheme.error, e, "\x1b[0m"
312 ));
313 } else {
314 result.push_str(&format!("arithmetic error: {}", e));
315 }
316 }
317 }
318 } else {
319 result.push_str("$((");
321 result.push_str(&arithmetic_expr);
322 }
324 continue;
325 }
326
327 let mut sub_command = String::new();
329 let mut paren_depth = 1;
330
331 for c in chars.by_ref() {
332 if c == '(' {
333 paren_depth += 1;
334 sub_command.push(c);
335 } else if c == ')' {
336 paren_depth -= 1;
337 if paren_depth == 0 {
338 break;
339 }
340 sub_command.push(c);
341 } else {
342 sub_command.push(c);
343 }
344 }
345
346 if let Ok(tokens) = crate::lexer::lex(&sub_command, shell_state) {
349 let expanded_tokens = match crate::lexer::expand_aliases(
351 tokens,
352 shell_state,
353 &mut std::collections::HashSet::new(),
354 ) {
355 Ok(t) => t,
356 Err(_) => {
357 result.push_str("$(");
359 result.push_str(&sub_command);
360 result.push(')');
361 continue;
362 }
363 };
364
365 match crate::parser::parse(expanded_tokens) {
366 Ok(ast) => {
367 match execute_and_capture_output(ast, shell_state) {
369 Ok(output) => {
370 result.push_str(&output);
371 }
372 Err(_) => {
373 result.push_str("$(");
375 result.push_str(&sub_command);
376 result.push(')');
377 }
378 }
379 }
380 Err(_parse_err) => {
381 let tokens_str = sub_command.trim();
383 if tokens_str.contains(' ') {
384 let parts: Vec<&str> = tokens_str.split_whitespace().collect();
386 if let Some(first_token) = parts.first()
387 && shell_state.get_function(first_token).is_some()
388 {
389 let function_call = Ast::FunctionCall {
391 name: first_token.to_string(),
392 args: parts[1..].iter().map(|s| s.to_string()).collect(),
393 };
394 match execute_and_capture_output(function_call, shell_state) {
395 Ok(output) => {
396 result.push_str(&output);
397 continue;
398 }
399 Err(_) => {
400 }
402 }
403 }
404 }
405 result.push_str("$(");
407 result.push_str(&sub_command);
408 result.push(')');
409 }
410 }
411 } else {
412 result.push_str("$(");
414 result.push_str(&sub_command);
415 result.push(')');
416 }
417 } else {
418 let mut var_name = String::new();
420 let mut next_ch = chars.peek();
421
422 if let Some(&c) = next_ch {
424 if c == '?' || c == '$' || c == '0' || c == '#' || c == '*' || c == '@' {
425 var_name.push(c);
426 chars.next(); } else if c.is_ascii_digit() {
428 var_name.push(c);
430 chars.next();
431 } else {
432 while let Some(&c) = next_ch {
434 if c.is_alphanumeric() || c == '_' {
435 var_name.push(c);
436 chars.next(); next_ch = chars.peek();
438 } else {
439 break;
440 }
441 }
442 }
443 }
444
445 if !var_name.is_empty() {
446 if let Some(value) = shell_state.get_var(&var_name) {
447 result.push_str(&value);
448 } else {
449 if var_name.chars().next().unwrap().is_ascii_digit()
452 || var_name == "?"
453 || var_name == "$"
454 || var_name == "0"
455 || var_name == "#"
456 || var_name == "*"
457 || var_name == "@"
458 {
459 } else {
461 result.push('$');
463 result.push_str(&var_name);
464 }
465 }
466 } else {
467 result.push('$');
468 }
469 }
470 } else if ch == '`' {
471 let mut sub_command = String::new();
473
474 for c in chars.by_ref() {
475 if c == '`' {
476 break;
477 }
478 sub_command.push(c);
479 }
480
481 if let Ok(tokens) = crate::lexer::lex(&sub_command, shell_state) {
483 let expanded_tokens = match crate::lexer::expand_aliases(
485 tokens,
486 shell_state,
487 &mut std::collections::HashSet::new(),
488 ) {
489 Ok(t) => t,
490 Err(_) => {
491 result.push('`');
493 result.push_str(&sub_command);
494 result.push('`');
495 continue;
496 }
497 };
498
499 if let Ok(ast) = crate::parser::parse(expanded_tokens) {
500 match execute_and_capture_output(ast, shell_state) {
502 Ok(output) => {
503 result.push_str(&output);
504 }
505 Err(_) => {
506 result.push('`');
508 result.push_str(&sub_command);
509 result.push('`');
510 }
511 }
512 } else {
513 result.push('`');
515 result.push_str(&sub_command);
516 result.push('`');
517 }
518 } else {
519 result.push('`');
521 result.push_str(&sub_command);
522 result.push('`');
523 }
524 } else {
525 result.push(ch);
526 }
527 }
528
529 result
530}
531
532fn expand_wildcards(args: &[String]) -> Result<Vec<String>, String> {
533 let mut expanded_args = Vec::new();
534
535 for arg in args {
536 if arg.contains('*') || arg.contains('?') || arg.contains('[') {
537 match glob::glob(arg) {
539 Ok(paths) => {
540 let mut matches: Vec<String> = paths
541 .filter_map(|p| p.ok())
542 .map(|p| p.to_string_lossy().to_string())
543 .collect();
544 if matches.is_empty() {
545 expanded_args.push(arg.clone());
547 } else {
548 matches.sort();
550 expanded_args.extend(matches);
551 }
552 }
553 Err(_e) => {
554 expanded_args.push(arg.clone());
556 }
557 }
558 } else {
559 expanded_args.push(arg.clone());
560 }
561 }
562 Ok(expanded_args)
563}
564
565fn collect_here_document_content(delimiter: &str, shell_state: &mut ShellState) -> String {
569 if let Some(content) = shell_state.pending_heredoc_content.take() {
571 return content;
572 }
573
574 let stdin = std::io::stdin();
576 let mut reader = BufReader::new(stdin.lock());
577 let mut content = String::new();
578 let mut line = String::new();
579
580 loop {
581 line.clear();
582 match reader.read_line(&mut line) {
583 Ok(0) => {
584 break;
586 }
587 Ok(_) => {
588 let line_content = line.trim_end();
590 if line_content == delimiter {
591 break;
593 } else {
594 content.push_str(&line);
596 }
597 }
598 Err(e) => {
599 if shell_state.colors_enabled {
600 eprintln!(
601 "{}Error reading here-document content: {}\x1b[0m",
602 shell_state.color_scheme.error, e
603 );
604 } else {
605 eprintln!("Error reading here-document content: {}", e);
606 }
607 break;
608 }
609 }
610 }
611
612 content
613}
614
615fn apply_redirections(
626 redirections: &[Redirection],
627 shell_state: &mut ShellState,
628 mut command: Option<&mut Command>,
629) -> Result<(), String> {
630 for redir in redirections {
632 match redir {
633 Redirection::Input(file) => {
634 apply_input_redirection(0, file, shell_state, command.as_deref_mut())?;
635 }
636 Redirection::Output(file) => {
637 apply_output_redirection(1, file, false, shell_state, command.as_deref_mut())?;
638 }
639 Redirection::Append(file) => {
640 apply_output_redirection(1, file, true, shell_state, command.as_deref_mut())?;
641 }
642 Redirection::FdInput(fd, file) => {
643 apply_input_redirection(*fd, file, shell_state, command.as_deref_mut())?;
644 }
645 Redirection::FdOutput(fd, file) => {
646 apply_output_redirection(*fd, file, false, shell_state, command.as_deref_mut())?;
647 }
648 Redirection::FdAppend(fd, file) => {
649 apply_output_redirection(*fd, file, true, shell_state, command.as_deref_mut())?;
650 }
651 Redirection::FdDuplicate(target_fd, source_fd) => {
652 apply_fd_duplication(*target_fd, *source_fd, shell_state, command.as_deref_mut())?;
653 }
654 Redirection::FdClose(fd) => {
655 apply_fd_close(*fd, shell_state, command.as_deref_mut())?;
656 }
657 Redirection::FdInputOutput(fd, file) => {
658 apply_fd_input_output(*fd, file, shell_state, command.as_deref_mut())?;
659 }
660 Redirection::HereDoc(delimiter, quoted_str) => {
661 let quoted = quoted_str == "true";
662 apply_heredoc_redirection(
663 0,
664 delimiter,
665 quoted,
666 shell_state,
667 command.as_deref_mut(),
668 )?;
669 }
670 Redirection::HereString(content) => {
671 apply_herestring_redirection(0, content, shell_state, command.as_deref_mut())?;
672 }
673 }
674 }
675 Ok(())
676}
677
678fn apply_input_redirection(
680 fd: i32,
681 file: &str,
682 shell_state: &mut ShellState,
683 command: Option<&mut Command>,
684) -> Result<(), String> {
685 let expanded_file = expand_variables_in_string(file, shell_state);
686
687 let file_handle =
689 File::open(&expanded_file).map_err(|e| format!("Cannot open {}: {}", expanded_file, e))?;
690
691 if fd == 0 {
692 if let Some(cmd) = command {
694 cmd.stdin(Stdio::from(file_handle));
695 }
696 } else {
699 let fd_file = File::open(&expanded_file)
702 .map_err(|e| format!("Cannot open {}: {}", expanded_file, e))?;
703
704 shell_state.fd_table.borrow_mut().open_fd(
706 fd,
707 &expanded_file,
708 true, false, false, false, )?;
713
714 if let Some(cmd) = command {
716 let target_fd = fd;
719 unsafe {
720 cmd.pre_exec(move || {
721 let raw_fd = fd_file.as_raw_fd();
722
723 if raw_fd != target_fd {
726 let result = libc::dup2(raw_fd, target_fd);
727 if result < 0 {
728 return Err(std::io::Error::last_os_error());
729 }
730 }
733 Ok(())
734 });
735 }
736 }
737 }
738
739 Ok(())
740}
741
742fn apply_output_redirection(
744 fd: i32,
745 file: &str,
746 append: bool,
747 shell_state: &mut ShellState,
748 command: Option<&mut Command>,
749) -> Result<(), String> {
750 let expanded_file = expand_variables_in_string(file, shell_state);
751
752 let file_handle = if append {
754 OpenOptions::new()
755 .append(true)
756 .create(true)
757 .open(&expanded_file)
758 .map_err(|e| format!("Cannot open {}: {}", expanded_file, e))?
759 } else {
760 File::create(&expanded_file)
761 .map_err(|e| format!("Cannot create {}: {}", expanded_file, e))?
762 };
763
764 if fd == 1 {
765 if let Some(cmd) = command {
767 cmd.stdout(Stdio::from(file_handle));
768 }
769 } else if fd == 2 {
770 if let Some(cmd) = command {
772 cmd.stderr(Stdio::from(file_handle));
773 }
774 } else {
775 shell_state.fd_table.borrow_mut().open_fd(
777 fd,
778 &expanded_file,
779 false, true, append,
782 !append, )?;
784 }
785
786 Ok(())
787}
788
789fn apply_fd_duplication(
791 target_fd: i32,
792 source_fd: i32,
793 shell_state: &mut ShellState,
794 _command: Option<&mut Command>,
795) -> Result<(), String> {
796 if shell_state.fd_table.borrow().is_closed(source_fd) {
798 let error_msg = format!("File descriptor {} is closed", source_fd);
799 if shell_state.colors_enabled {
800 eprintln!(
801 "{}Redirection error: {}\x1b[0m",
802 shell_state.color_scheme.error, error_msg
803 );
804 } else {
805 eprintln!("Redirection error: {}", error_msg);
806 }
807 return Err(error_msg);
808 }
809
810 shell_state
812 .fd_table
813 .borrow_mut()
814 .duplicate_fd(source_fd, target_fd)?;
815 Ok(())
816}
817
818fn apply_fd_close(
820 fd: i32,
821 shell_state: &mut ShellState,
822 command: Option<&mut Command>,
823) -> Result<(), String> {
824 shell_state.fd_table.borrow_mut().close_fd(fd)?;
826
827 if let Some(cmd) = command {
830 match fd {
831 0 => {
832 cmd.stdin(Stdio::null());
834 }
835 1 => {
836 cmd.stdout(Stdio::null());
838 }
839 2 => {
840 cmd.stderr(Stdio::null());
842 }
843 _ => {
844 }
847 }
848 }
849
850 Ok(())
851}
852
853fn apply_fd_input_output(
855 fd: i32,
856 file: &str,
857 shell_state: &mut ShellState,
858 _command: Option<&mut Command>,
859) -> Result<(), String> {
860 let expanded_file = expand_variables_in_string(file, shell_state);
861
862 shell_state.fd_table.borrow_mut().open_fd(
864 fd,
865 &expanded_file,
866 true, true, false, false, )?;
871
872 Ok(())
873}
874
875fn apply_heredoc_redirection(
877 fd: i32,
878 delimiter: &str,
879 quoted: bool,
880 shell_state: &mut ShellState,
881 command: Option<&mut Command>,
882) -> Result<(), String> {
883 let here_doc_content = collect_here_document_content(delimiter, shell_state);
884
885 let expanded_content = if quoted {
887 here_doc_content
888 } else {
889 expand_variables_in_string(&here_doc_content, shell_state)
890 };
891
892 let (reader, mut writer) =
894 pipe().map_err(|e| format!("Failed to create pipe for here-document: {}", e))?;
895
896 writeln!(writer, "{}", expanded_content)
897 .map_err(|e| format!("Failed to write here-document content: {}", e))?;
898
899 if fd == 0 {
901 if let Some(cmd) = command {
902 cmd.stdin(Stdio::from(reader));
903 }
904 }
905
906 Ok(())
907}
908
909fn apply_herestring_redirection(
911 fd: i32,
912 content: &str,
913 shell_state: &mut ShellState,
914 command: Option<&mut Command>,
915) -> Result<(), String> {
916 let expanded_content = expand_variables_in_string(content, shell_state);
917
918 let (reader, mut writer) =
920 pipe().map_err(|e| format!("Failed to create pipe for here-string: {}", e))?;
921
922 write!(writer, "{}", expanded_content)
923 .map_err(|e| format!("Failed to write here-string content: {}", e))?;
924
925 if fd == 0 {
927 if let Some(cmd) = command {
928 cmd.stdin(Stdio::from(reader));
929 }
930 }
931
932 Ok(())
933}
934
935pub fn execute_trap_handler(trap_cmd: &str, shell_state: &mut ShellState) -> i32 {
938 let saved_exit_code = shell_state.last_exit_code;
940
941 let result = match crate::lexer::lex(trap_cmd, shell_state) {
947 Ok(tokens) => {
948 match crate::lexer::expand_aliases(
949 tokens,
950 shell_state,
951 &mut std::collections::HashSet::new(),
952 ) {
953 Ok(expanded_tokens) => {
954 match crate::parser::parse(expanded_tokens) {
955 Ok(ast) => execute(ast, shell_state),
956 Err(_) => {
957 saved_exit_code
959 }
960 }
961 }
962 Err(_) => {
963 saved_exit_code
965 }
966 }
967 }
968 Err(_) => {
969 saved_exit_code
971 }
972 };
973
974 shell_state.last_exit_code = saved_exit_code;
976
977 result
978}
979
980pub fn execute(ast: Ast, shell_state: &mut ShellState) -> i32 {
981 match ast {
982 Ast::Assignment { var, value } => {
983 let expanded_value = expand_variables_in_string(&value, shell_state);
985 shell_state.set_var(&var, expanded_value);
986 0
987 }
988 Ast::LocalAssignment { var, value } => {
989 let expanded_value = expand_variables_in_string(&value, shell_state);
991 shell_state.set_local_var(&var, expanded_value);
992 0
993 }
994 Ast::Pipeline(commands) => {
995 if commands.is_empty() {
996 return 0;
997 }
998
999 if commands.len() == 1 {
1000 execute_single_command(&commands[0], shell_state)
1002 } else {
1003 execute_pipeline(&commands, shell_state)
1005 }
1006 }
1007 Ast::Sequence(asts) => {
1008 let mut exit_code = 0;
1009 for ast in asts {
1010 exit_code = execute(ast, shell_state);
1011
1012 if shell_state.is_returning() {
1014 return exit_code;
1015 }
1016
1017 if shell_state.exit_requested {
1019 return shell_state.exit_code;
1020 }
1021 }
1022 exit_code
1023 }
1024 Ast::If {
1025 branches,
1026 else_branch,
1027 } => {
1028 for (condition, then_branch) in branches {
1029 let cond_exit = execute(*condition, shell_state);
1030 if cond_exit == 0 {
1031 let exit_code = execute(*then_branch, shell_state);
1032
1033 if shell_state.is_returning() {
1035 return exit_code;
1036 }
1037
1038 return exit_code;
1039 }
1040 }
1041 if let Some(else_b) = else_branch {
1042 let exit_code = execute(*else_b, shell_state);
1043
1044 if shell_state.is_returning() {
1046 return exit_code;
1047 }
1048
1049 exit_code
1050 } else {
1051 0
1052 }
1053 }
1054 Ast::Case {
1055 word,
1056 cases,
1057 default,
1058 } => {
1059 for (patterns, branch) in cases {
1060 for pattern in &patterns {
1061 if let Ok(glob_pattern) = glob::Pattern::new(pattern) {
1062 if glob_pattern.matches(&word) {
1063 let exit_code = execute(branch, shell_state);
1064
1065 if shell_state.is_returning() {
1067 return exit_code;
1068 }
1069
1070 return exit_code;
1071 }
1072 } else {
1073 if &word == pattern {
1075 let exit_code = execute(branch, shell_state);
1076
1077 if shell_state.is_returning() {
1079 return exit_code;
1080 }
1081
1082 return exit_code;
1083 }
1084 }
1085 }
1086 }
1087 if let Some(def) = default {
1088 let exit_code = execute(*def, shell_state);
1089
1090 if shell_state.is_returning() {
1092 return exit_code;
1093 }
1094
1095 exit_code
1096 } else {
1097 0
1098 }
1099 }
1100 Ast::For {
1101 variable,
1102 items,
1103 body,
1104 } => {
1105 let mut exit_code = 0;
1106
1107 for item in items {
1109 crate::state::process_pending_signals(shell_state);
1111
1112 if shell_state.exit_requested {
1114 return shell_state.exit_code;
1115 }
1116
1117 shell_state.set_var(&variable, item.clone());
1119
1120 exit_code = execute(*body.clone(), shell_state);
1122
1123 if shell_state.is_returning() {
1125 return exit_code;
1126 }
1127
1128 if shell_state.exit_requested {
1130 return shell_state.exit_code;
1131 }
1132 }
1133
1134 exit_code
1135 }
1136 Ast::While { condition, body } => {
1137 let mut exit_code = 0;
1138
1139 loop {
1141 let cond_exit = execute(*condition.clone(), shell_state);
1143
1144 if shell_state.is_returning() {
1146 return cond_exit;
1147 }
1148
1149 if shell_state.exit_requested {
1151 return shell_state.exit_code;
1152 }
1153
1154 if cond_exit != 0 {
1156 break;
1157 }
1158
1159 exit_code = execute(*body.clone(), shell_state);
1161
1162 if shell_state.is_returning() {
1164 return exit_code;
1165 }
1166
1167 if shell_state.exit_requested {
1169 return shell_state.exit_code;
1170 }
1171 }
1172
1173 exit_code
1174 }
1175 Ast::FunctionDefinition { name, body } => {
1176 shell_state.define_function(name.clone(), *body);
1178 0
1179 }
1180 Ast::FunctionCall { name, args } => {
1181 if let Some(function_body) = shell_state.get_function(&name).cloned() {
1182 if shell_state.function_depth >= shell_state.max_recursion_depth {
1184 eprintln!(
1185 "Function recursion limit ({}) exceeded",
1186 shell_state.max_recursion_depth
1187 );
1188 return 1;
1189 }
1190
1191 shell_state.enter_function();
1193
1194 let old_positional = shell_state.positional_params.clone();
1196
1197 shell_state.set_positional_params(args.clone());
1199
1200 let exit_code = execute(function_body, shell_state);
1202
1203 if shell_state.is_returning() {
1205 let return_value = shell_state.get_return_value().unwrap_or(0);
1206
1207 shell_state.set_positional_params(old_positional);
1209
1210 shell_state.exit_function();
1212
1213 shell_state.clear_return();
1215
1216 return return_value;
1218 }
1219
1220 shell_state.set_positional_params(old_positional);
1222
1223 shell_state.exit_function();
1225
1226 exit_code
1227 } else {
1228 eprintln!("Function '{}' not found", name);
1229 1
1230 }
1231 }
1232 Ast::Return { value } => {
1233 if shell_state.function_depth == 0 {
1235 eprintln!("Return statement outside of function");
1236 return 1;
1237 }
1238
1239 let exit_code = if let Some(ref val) = value {
1241 val.parse::<i32>().unwrap_or(0)
1242 } else {
1243 0
1244 };
1245
1246 shell_state.set_return(exit_code);
1248
1249 exit_code
1251 }
1252 Ast::And { left, right } => {
1253 let left_exit = execute(*left, shell_state);
1255
1256 if shell_state.is_returning() {
1258 return left_exit;
1259 }
1260
1261 if left_exit == 0 {
1263 execute(*right, shell_state)
1264 } else {
1265 left_exit
1266 }
1267 }
1268 Ast::Or { left, right } => {
1269 let left_exit = execute(*left, shell_state);
1271
1272 if shell_state.is_returning() {
1274 return left_exit;
1275 }
1276
1277 if left_exit != 0 {
1279 execute(*right, shell_state)
1280 } else {
1281 left_exit
1282 }
1283 }
1284 Ast::Subshell { body } => execute_subshell(*body, shell_state),
1285 }
1286}
1287
1288fn execute_single_command(cmd: &ShellCommand, shell_state: &mut ShellState) -> i32 {
1289 if let Some(ref compound_ast) = cmd.compound {
1291 return execute_compound_with_redirections(compound_ast, shell_state, &cmd.redirections);
1293 }
1294
1295 if cmd.args.is_empty() {
1296 if !cmd.redirections.is_empty() {
1298 if let Err(e) = apply_redirections(&cmd.redirections, shell_state, None) {
1299 if shell_state.colors_enabled {
1300 eprintln!(
1301 "{}Redirection error: {}\x1b[0m",
1302 shell_state.color_scheme.error, e
1303 );
1304 } else {
1305 eprintln!("Redirection error: {}", e);
1306 }
1307 return 1;
1308 }
1309 }
1310 return 0;
1311 }
1312
1313 let var_expanded_args = expand_variables_in_args(&cmd.args, shell_state);
1315 let expanded_args = match expand_wildcards(&var_expanded_args) {
1316 Ok(args) => args,
1317 Err(_) => return 1,
1318 };
1319
1320 if expanded_args.is_empty() {
1321 return 0;
1322 }
1323
1324 if shell_state.get_function(&expanded_args[0]).is_some() {
1326 let function_call = Ast::FunctionCall {
1328 name: expanded_args[0].clone(),
1329 args: expanded_args[1..].to_vec(),
1330 };
1331 return execute(function_call, shell_state);
1332 }
1333
1334 if crate::builtins::is_builtin(&expanded_args[0]) {
1335 let temp_cmd = ShellCommand {
1337 args: expanded_args,
1338 redirections: cmd.redirections.clone(),
1339 compound: None,
1340 };
1341
1342 if let Some(ref capture_buffer) = shell_state.capture_output.clone() {
1344 struct CaptureWriter {
1346 buffer: Rc<RefCell<Vec<u8>>>,
1347 }
1348 impl std::io::Write for CaptureWriter {
1349 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1350 self.buffer.borrow_mut().extend_from_slice(buf);
1351 Ok(buf.len())
1352 }
1353 fn flush(&mut self) -> std::io::Result<()> {
1354 Ok(())
1355 }
1356 }
1357 let writer = CaptureWriter {
1358 buffer: capture_buffer.clone(),
1359 };
1360 crate::builtins::execute_builtin(&temp_cmd, shell_state, Some(Box::new(writer)))
1361 } else {
1362 crate::builtins::execute_builtin(&temp_cmd, shell_state, None)
1363 }
1364 } else {
1365 let mut env_assignments = Vec::new();
1368 let mut command_start_idx = 0;
1369
1370 for (idx, arg) in expanded_args.iter().enumerate() {
1371 if let Some(eq_pos) = arg.find('=')
1373 && eq_pos > 0
1374 {
1375 let var_part = &arg[..eq_pos];
1376 if var_part
1378 .chars()
1379 .next()
1380 .map(|c| c.is_alphabetic() || c == '_')
1381 .unwrap_or(false)
1382 && var_part.chars().all(|c| c.is_alphanumeric() || c == '_')
1383 {
1384 env_assignments.push(arg.clone());
1385 command_start_idx = idx + 1;
1386 continue;
1387 }
1388 }
1389 break;
1391 }
1392
1393 let has_command = command_start_idx < expanded_args.len();
1395
1396 if !has_command {
1399 for assignment in &env_assignments {
1400 if let Some(eq_pos) = assignment.find('=') {
1401 let var_name = &assignment[..eq_pos];
1402 let var_value = &assignment[eq_pos + 1..];
1403 shell_state.set_var(var_name, var_value.to_string());
1404 }
1405 }
1406
1407 if !cmd.redirections.is_empty() {
1409 if let Err(e) = apply_redirections(&cmd.redirections, shell_state, None) {
1410 if shell_state.colors_enabled {
1411 eprintln!(
1412 "{}Redirection error: {}\x1b[0m",
1413 shell_state.color_scheme.error, e
1414 );
1415 } else {
1416 eprintln!("Redirection error: {}", e);
1417 }
1418 return 1;
1419 }
1420 }
1421 return 0;
1422 }
1423
1424 let mut command = Command::new(&expanded_args[command_start_idx]);
1426 command.args(&expanded_args[command_start_idx + 1..]);
1427
1428 if let Some(fd) = shell_state.stdin_override {
1430 unsafe {
1431 let dup_fd = libc::dup(fd);
1432 if dup_fd >= 0 {
1433 command.stdin(Stdio::from_raw_fd(dup_fd));
1434 }
1435 }
1436 }
1437
1438 let mut child_env = shell_state.get_env_for_child();
1440
1441 for assignment in env_assignments {
1443 if let Some(eq_pos) = assignment.find('=') {
1444 let var_name = assignment[..eq_pos].to_string();
1445 let var_value = assignment[eq_pos + 1..].to_string();
1446 child_env.insert(var_name, var_value);
1447 }
1448 }
1449
1450 command.env_clear();
1451 for (key, value) in child_env {
1452 command.env(key, value);
1453 }
1454
1455 let capturing = shell_state.capture_output.is_some();
1457 if capturing {
1458 command.stdout(Stdio::piped());
1459 }
1460
1461 if let Err(e) = apply_redirections(&cmd.redirections, shell_state, Some(&mut command)) {
1463 if shell_state.colors_enabled {
1464 eprintln!(
1465 "{}Redirection error: {}\x1b[0m",
1466 shell_state.color_scheme.error, e
1467 );
1468 } else {
1469 eprintln!("Redirection error: {}", e);
1470 }
1471 return 1;
1472 }
1473
1474 let custom_fds: Vec<(i32, RawFd)> = {
1478 let fd_table = shell_state.fd_table.borrow();
1479 let mut fds = Vec::new();
1480
1481 for fd_num in 3..=9 {
1482 if fd_table.is_open(fd_num) {
1483 if let Some(raw_fd) = fd_table.get_raw_fd(fd_num) {
1484 fds.push((fd_num, raw_fd));
1485 }
1486 }
1487 }
1488
1489 fds
1490 };
1491
1492 if !custom_fds.is_empty() {
1494 unsafe {
1495 command.pre_exec(move || {
1496 for (target_fd, source_fd) in &custom_fds {
1497 let result = libc::dup2(*source_fd, *target_fd);
1498 if result < 0 {
1499 return Err(std::io::Error::last_os_error());
1500 }
1501 }
1502 Ok(())
1503 });
1504 }
1505 }
1506
1507 match command.spawn() {
1511 Ok(mut child) => {
1512 if capturing {
1514 if let Some(mut stdout) = child.stdout.take() {
1515 use std::io::Read;
1516 let mut output = Vec::new();
1517 if stdout.read_to_end(&mut output).is_ok() {
1518 if let Some(ref capture_buffer) = shell_state.capture_output {
1519 capture_buffer.borrow_mut().extend_from_slice(&output);
1520 }
1521 }
1522 }
1523 }
1524
1525 match child.wait() {
1526 Ok(status) => status.code().unwrap_or(0),
1527 Err(e) => {
1528 if shell_state.colors_enabled {
1529 eprintln!(
1530 "{}Error waiting for command: {}\x1b[0m",
1531 shell_state.color_scheme.error, e
1532 );
1533 } else {
1534 eprintln!("Error waiting for command: {}", e);
1535 }
1536 1
1537 }
1538 }
1539 }
1540 Err(e) => {
1541 if shell_state.colors_enabled {
1542 eprintln!(
1543 "{}Command spawn error: {}\x1b[0m",
1544 shell_state.color_scheme.error, e
1545 );
1546 } else {
1547 eprintln!("Command spawn error: {}", e);
1548 }
1549 1
1550 }
1551 }
1552 }
1553}
1554
1555fn execute_pipeline(commands: &[ShellCommand], shell_state: &mut ShellState) -> i32 {
1556 let mut exit_code = 0;
1557 let mut previous_stdout = None;
1558
1559 for (i, cmd) in commands.iter().enumerate() {
1560 let is_last = i == commands.len() - 1;
1561
1562 if let Some(ref compound_ast) = cmd.compound {
1564 exit_code = execute_compound_in_pipeline(
1566 compound_ast,
1567 shell_state,
1568 i == 0,
1569 is_last,
1570 &cmd.redirections,
1571 );
1572
1573 previous_stdout = None;
1576 continue;
1577 }
1578
1579 if cmd.args.is_empty() {
1580 continue;
1581 }
1582
1583 let var_expanded_args = expand_variables_in_args(&cmd.args, shell_state);
1585 let expanded_args = match expand_wildcards(&var_expanded_args) {
1586 Ok(args) => args,
1587 Err(_) => return 1,
1588 };
1589
1590 if expanded_args.is_empty() {
1591 continue;
1592 }
1593
1594 if crate::builtins::is_builtin(&expanded_args[0]) {
1595 let temp_cmd = ShellCommand {
1598 args: expanded_args,
1599 redirections: cmd.redirections.clone(),
1600 compound: None,
1601 };
1602 if !is_last {
1603 let (reader, writer) = match pipe() {
1605 Ok(p) => p,
1606 Err(e) => {
1607 if shell_state.colors_enabled {
1608 eprintln!(
1609 "{}Error creating pipe for builtin: {}\x1b[0m",
1610 shell_state.color_scheme.error, e
1611 );
1612 } else {
1613 eprintln!("Error creating pipe for builtin: {}", e);
1614 }
1615 return 1;
1616 }
1617 };
1618 exit_code = crate::builtins::execute_builtin(
1620 &temp_cmd,
1621 shell_state,
1622 Some(Box::new(writer)),
1623 );
1624 previous_stdout = Some(Stdio::from(reader));
1626 } else {
1627 if let Some(ref capture_buffer) = shell_state.capture_output.clone() {
1629 struct CaptureWriter {
1631 buffer: Rc<RefCell<Vec<u8>>>,
1632 }
1633 impl std::io::Write for CaptureWriter {
1634 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1635 self.buffer.borrow_mut().extend_from_slice(buf);
1636 Ok(buf.len())
1637 }
1638 fn flush(&mut self) -> std::io::Result<()> {
1639 Ok(())
1640 }
1641 }
1642 let writer = CaptureWriter {
1643 buffer: capture_buffer.clone(),
1644 };
1645 exit_code = crate::builtins::execute_builtin(
1646 &temp_cmd,
1647 shell_state,
1648 Some(Box::new(writer)),
1649 );
1650 } else {
1651 exit_code = crate::builtins::execute_builtin(&temp_cmd, shell_state, None);
1653 }
1654 previous_stdout = None;
1655 }
1656 } else {
1657 let mut command = Command::new(&expanded_args[0]);
1658 command.args(&expanded_args[1..]);
1659
1660 let child_env = shell_state.get_env_for_child();
1662 command.env_clear();
1663 for (key, value) in child_env {
1664 command.env(key, value);
1665 }
1666
1667 if let Some(prev) = previous_stdout.take() {
1669 command.stdin(prev);
1670 } else if i > 0 {
1671 command.stdin(Stdio::null());
1675 } else if let Some(fd) = shell_state.stdin_override {
1676 unsafe {
1679 let dup_fd = libc::dup(fd);
1680 if dup_fd >= 0 {
1681 command.stdin(Stdio::from_raw_fd(dup_fd));
1682 }
1683 }
1684 }
1685
1686 if !is_last {
1688 command.stdout(Stdio::piped());
1689 } else if shell_state.capture_output.is_some() {
1690 command.stdout(Stdio::piped());
1692 }
1693
1694 if let Err(e) = apply_redirections(&cmd.redirections, shell_state, Some(&mut command)) {
1696 if shell_state.colors_enabled {
1697 eprintln!(
1698 "{}Redirection error: {}\x1b[0m",
1699 shell_state.color_scheme.error, e
1700 );
1701 } else {
1702 eprintln!("Redirection error: {}", e);
1703 }
1704 return 1;
1705 }
1706
1707 match command.spawn() {
1708 Ok(mut child) => {
1709 if !is_last {
1710 previous_stdout = child.stdout.take().map(Stdio::from);
1711 } else if shell_state.capture_output.is_some() {
1712 if let Some(mut stdout) = child.stdout.take() {
1714 use std::io::Read;
1715 let mut output = Vec::new();
1716 if stdout.read_to_end(&mut output).is_ok()
1717 && let Some(ref capture_buffer) = shell_state.capture_output
1718 {
1719 capture_buffer.borrow_mut().extend_from_slice(&output);
1720 }
1721 }
1722 }
1723 match child.wait() {
1724 Ok(status) => {
1725 exit_code = status.code().unwrap_or(0);
1726 }
1727 Err(e) => {
1728 if shell_state.colors_enabled {
1729 eprintln!(
1730 "{}Error waiting for command: {}\x1b[0m",
1731 shell_state.color_scheme.error, e
1732 );
1733 } else {
1734 eprintln!("Error waiting for command: {}", e);
1735 }
1736 exit_code = 1;
1737 }
1738 }
1739 }
1740 Err(e) => {
1741 if shell_state.colors_enabled {
1742 eprintln!(
1743 "{}Error spawning command '{}{}",
1744 shell_state.color_scheme.error,
1745 expanded_args[0],
1746 &format!("': {}\x1b[0m", e)
1747 );
1748 } else {
1749 eprintln!("Error spawning command '{}': {}", expanded_args[0], e);
1750 }
1751 exit_code = 1;
1752 }
1753 }
1754 }
1755 }
1756
1757 exit_code
1758}
1759
1760fn execute_subshell(body: Ast, shell_state: &mut ShellState) -> i32 {
1778 if shell_state.subshell_depth >= MAX_SUBSHELL_DEPTH {
1780 if shell_state.colors_enabled {
1781 eprintln!(
1782 "{}Subshell nesting limit ({}) exceeded\x1b[0m",
1783 shell_state.color_scheme.error, MAX_SUBSHELL_DEPTH
1784 );
1785 } else {
1786 eprintln!("Subshell nesting limit ({}) exceeded", MAX_SUBSHELL_DEPTH);
1787 }
1788 shell_state.last_exit_code = 1;
1789 return 1;
1790 }
1791
1792 let original_dir = std::env::current_dir().ok();
1794
1795 let mut subshell_state = shell_state.clone();
1797
1798 subshell_state.subshell_depth = shell_state.subshell_depth + 1;
1800
1801 let parent_traps = shell_state.trap_handlers.lock().unwrap().clone();
1803 subshell_state.trap_handlers = std::sync::Arc::new(std::sync::Mutex::new(parent_traps));
1804
1805 let exit_code = execute(body, &mut subshell_state);
1807
1808 let final_exit_code = if subshell_state.exit_requested {
1811 subshell_state.exit_code
1813 } else if subshell_state.is_returning() {
1814 subshell_state.get_return_value().unwrap_or(exit_code)
1817 } else {
1818 exit_code
1819 };
1820
1821 subshell_state.fd_table.borrow_mut().clear();
1824
1825 if let Some(dir) = original_dir {
1827 let _ = std::env::set_current_dir(dir);
1828 }
1829
1830 shell_state.last_exit_code = final_exit_code;
1832
1833 final_exit_code
1835}
1836
1837fn execute_compound_with_redirections(
1847 compound_ast: &Ast,
1848 shell_state: &mut ShellState,
1849 redirections: &[Redirection],
1850) -> i32 {
1851 match compound_ast {
1852 Ast::Subshell { body } => {
1853 let has_output_redir = redirections.iter().any(|r| {
1860 matches!(
1861 r,
1862 Redirection::Output(_)
1863 | Redirection::Append(_)
1864 | Redirection::FdOutput(_, _)
1865 | Redirection::FdAppend(_, _)
1866 )
1867 });
1868
1869 if has_output_redir {
1870 let mut subshell_state = shell_state.clone();
1872
1873 let capture_buffer = Rc::new(RefCell::new(Vec::new()));
1875 subshell_state.capture_output = Some(capture_buffer.clone());
1876
1877 let exit_code = execute(*body.clone(), &mut subshell_state);
1879
1880 let output = capture_buffer.borrow().clone();
1882
1883 for redir in redirections {
1885 match redir {
1886 Redirection::Output(file) => {
1887 let expanded_file = expand_variables_in_string(file, shell_state);
1888 if let Err(e) = std::fs::write(&expanded_file, &output) {
1889 if shell_state.colors_enabled {
1890 eprintln!(
1891 "{}Redirection error: {}\x1b[0m",
1892 shell_state.color_scheme.error, e
1893 );
1894 } else {
1895 eprintln!("Redirection error: {}", e);
1896 }
1897 return 1;
1898 }
1899 }
1900 Redirection::Append(file) => {
1901 let expanded_file = expand_variables_in_string(file, shell_state);
1902 use std::fs::OpenOptions;
1903 let mut file_handle = match OpenOptions::new()
1904 .append(true)
1905 .create(true)
1906 .open(&expanded_file)
1907 {
1908 Ok(f) => f,
1909 Err(e) => {
1910 if shell_state.colors_enabled {
1911 eprintln!(
1912 "{}Redirection error: {}\x1b[0m",
1913 shell_state.color_scheme.error, e
1914 );
1915 } else {
1916 eprintln!("Redirection error: {}", e);
1917 }
1918 return 1;
1919 }
1920 };
1921 if let Err(e) = file_handle.write_all(&output) {
1922 if shell_state.colors_enabled {
1923 eprintln!(
1924 "{}Redirection error: {}\x1b[0m",
1925 shell_state.color_scheme.error, e
1926 );
1927 } else {
1928 eprintln!("Redirection error: {}", e);
1929 }
1930 return 1;
1931 }
1932 }
1933 _ => {
1934 }
1937 }
1938 }
1939
1940 shell_state.last_exit_code = exit_code;
1941 exit_code
1942 } else {
1943 execute_subshell(*body.clone(), shell_state)
1945 }
1946 }
1947 _ => {
1948 eprintln!("Unsupported compound command type");
1949 1
1950 }
1951 }
1952}
1953
1954fn execute_compound_in_pipeline(
1965 compound_ast: &Ast,
1966 shell_state: &mut ShellState,
1967 is_first: bool,
1968 is_last: bool,
1969 _redirections: &[Redirection],
1970) -> i32 {
1971 match compound_ast {
1972 Ast::Subshell { body } => {
1973 let mut subshell_state = shell_state.clone();
1975
1976 let _null_file = if !is_first {
1980 if let Ok(f) = File::open("/dev/null") {
1981 subshell_state.stdin_override = Some(f.as_raw_fd());
1982 Some(f)
1983 } else {
1984 None
1985 }
1986 } else {
1987 None
1988 };
1989
1990 let exit_code = if !is_last || shell_state.capture_output.is_some() {
1992 let capture_buffer = Rc::new(RefCell::new(Vec::new()));
1994 subshell_state.capture_output = Some(capture_buffer.clone());
1995
1996 let code = execute(*body.clone(), &mut subshell_state);
1998
1999 if let Some(ref parent_capture) = shell_state.capture_output {
2001 let captured = capture_buffer.borrow().clone();
2002 parent_capture.borrow_mut().extend_from_slice(&captured);
2003 }
2004
2005 shell_state.last_exit_code = code;
2007
2008 code
2009 } else {
2010 let code = execute(*body.clone(), &mut subshell_state);
2012 shell_state.last_exit_code = code;
2013 code
2014 };
2015
2016 exit_code
2017 }
2018 _ => {
2019 eprintln!("Unsupported compound command in pipeline");
2021 1
2022 }
2023 }
2024}
2025
2026#[cfg(test)]
2027mod tests {
2028 use super::*;
2029 use std::sync::Mutex;
2030
2031 static ENV_LOCK: Mutex<()> = Mutex::new(());
2033
2034 #[test]
2035 fn test_execute_single_command_builtin() {
2036 let cmd = ShellCommand {
2037 args: vec!["true".to_string()],
2038 redirections: Vec::new(),
2039 compound: None,
2040 };
2041 let mut shell_state = ShellState::new();
2042 let exit_code = execute_single_command(&cmd, &mut shell_state);
2043 assert_eq!(exit_code, 0);
2044 }
2045
2046 #[test]
2048 fn test_execute_single_command_external() {
2049 let cmd = ShellCommand {
2050 args: vec!["true".to_string()], redirections: Vec::new(),
2052 compound: None,
2053 };
2054 let mut shell_state = ShellState::new();
2055 let exit_code = execute_single_command(&cmd, &mut shell_state);
2056 assert_eq!(exit_code, 0);
2057 }
2058
2059 #[test]
2060 fn test_execute_single_command_external_nonexistent() {
2061 let cmd = ShellCommand {
2062 args: vec!["nonexistent_command".to_string()],
2063 redirections: Vec::new(),
2064 compound: None,
2065 };
2066 let mut shell_state = ShellState::new();
2067 let exit_code = execute_single_command(&cmd, &mut shell_state);
2068 assert_eq!(exit_code, 1); }
2070
2071 #[test]
2072 fn test_execute_pipeline() {
2073 let commands = vec![
2074 ShellCommand {
2075 args: vec!["printf".to_string(), "hello".to_string()],
2076 redirections: Vec::new(),
2077 compound: None,
2078 },
2079 ShellCommand {
2080 args: vec!["cat".to_string()], redirections: Vec::new(),
2082 compound: None,
2083 },
2084 ];
2085 let mut shell_state = ShellState::new();
2086 let exit_code = execute_pipeline(&commands, &mut shell_state);
2087 assert_eq!(exit_code, 0);
2088 }
2089
2090 #[test]
2091 fn test_execute_empty_pipeline() {
2092 let commands = vec![];
2093 let mut shell_state = ShellState::new();
2094 let exit_code = execute(Ast::Pipeline(commands), &mut shell_state);
2095 assert_eq!(exit_code, 0);
2096 }
2097
2098 #[test]
2099 fn test_execute_single_command() {
2100 let ast = Ast::Pipeline(vec![ShellCommand {
2101 args: vec!["true".to_string()],
2102 redirections: Vec::new(),
2103 compound: None,
2104 }]);
2105 let mut shell_state = ShellState::new();
2106 let exit_code = execute(ast, &mut shell_state);
2107 assert_eq!(exit_code, 0);
2108 }
2109
2110 #[test]
2111 fn test_execute_function_definition() {
2112 let ast = Ast::FunctionDefinition {
2113 name: "test_func".to_string(),
2114 body: Box::new(Ast::Pipeline(vec![ShellCommand {
2115 args: vec!["echo".to_string(), "hello".to_string()],
2116 redirections: Vec::new(),
2117 compound: None,
2118 }])),
2119 };
2120 let mut shell_state = ShellState::new();
2121 let exit_code = execute(ast, &mut shell_state);
2122 assert_eq!(exit_code, 0);
2123
2124 assert!(shell_state.get_function("test_func").is_some());
2126 }
2127
2128 #[test]
2129 fn test_execute_function_call() {
2130 let mut shell_state = ShellState::new();
2132 shell_state.define_function(
2133 "test_func".to_string(),
2134 Ast::Pipeline(vec![ShellCommand {
2135 args: vec!["echo".to_string(), "hello".to_string()],
2136 redirections: Vec::new(),
2137 compound: None,
2138 }]),
2139 );
2140
2141 let ast = Ast::FunctionCall {
2143 name: "test_func".to_string(),
2144 args: vec![],
2145 };
2146 let exit_code = execute(ast, &mut shell_state);
2147 assert_eq!(exit_code, 0);
2148 }
2149
2150 #[test]
2151 fn test_execute_function_call_with_args() {
2152 let mut shell_state = ShellState::new();
2154 shell_state.define_function(
2155 "test_func".to_string(),
2156 Ast::Pipeline(vec![ShellCommand {
2157 args: vec!["echo".to_string(), "arg1".to_string()],
2158 redirections: Vec::new(),
2159 compound: None,
2160 }]),
2161 );
2162
2163 let ast = Ast::FunctionCall {
2165 name: "test_func".to_string(),
2166 args: vec!["hello".to_string()],
2167 };
2168 let exit_code = execute(ast, &mut shell_state);
2169 assert_eq!(exit_code, 0);
2170 }
2171
2172 #[test]
2173 fn test_execute_nonexistent_function() {
2174 let mut shell_state = ShellState::new();
2175 let ast = Ast::FunctionCall {
2176 name: "nonexistent".to_string(),
2177 args: vec![],
2178 };
2179 let exit_code = execute(ast, &mut shell_state);
2180 assert_eq!(exit_code, 1); }
2182
2183 #[test]
2184 fn test_execute_function_integration() {
2185 let mut shell_state = ShellState::new();
2187
2188 let define_ast = Ast::FunctionDefinition {
2190 name: "hello".to_string(),
2191 body: Box::new(Ast::Pipeline(vec![ShellCommand {
2192 args: vec!["printf".to_string(), "Hello from function".to_string()],
2193 redirections: Vec::new(),
2194 compound: None,
2195 }])),
2196 };
2197 let exit_code = execute(define_ast, &mut shell_state);
2198 assert_eq!(exit_code, 0);
2199
2200 let call_ast = Ast::FunctionCall {
2202 name: "hello".to_string(),
2203 args: vec![],
2204 };
2205 let exit_code = execute(call_ast, &mut shell_state);
2206 assert_eq!(exit_code, 0);
2207 }
2208
2209 #[test]
2210 fn test_execute_function_with_local_variables() {
2211 let mut shell_state = ShellState::new();
2212
2213 shell_state.set_var("global_var", "global_value".to_string());
2215
2216 let define_ast = Ast::FunctionDefinition {
2218 name: "test_func".to_string(),
2219 body: Box::new(Ast::Sequence(vec![
2220 Ast::LocalAssignment {
2221 var: "local_var".to_string(),
2222 value: "local_value".to_string(),
2223 },
2224 Ast::Assignment {
2225 var: "global_var".to_string(),
2226 value: "modified_in_function".to_string(),
2227 },
2228 Ast::Pipeline(vec![ShellCommand {
2229 args: vec!["printf".to_string(), "success".to_string()],
2230 redirections: Vec::new(),
2231 compound: None,
2232 }]),
2233 ])),
2234 };
2235 let exit_code = execute(define_ast, &mut shell_state);
2236 assert_eq!(exit_code, 0);
2237
2238 assert_eq!(
2240 shell_state.get_var("global_var"),
2241 Some("global_value".to_string())
2242 );
2243
2244 let call_ast = Ast::FunctionCall {
2246 name: "test_func".to_string(),
2247 args: vec![],
2248 };
2249 let exit_code = execute(call_ast, &mut shell_state);
2250 assert_eq!(exit_code, 0);
2251
2252 assert_eq!(
2254 shell_state.get_var("global_var"),
2255 Some("modified_in_function".to_string())
2256 );
2257 }
2258
2259 #[test]
2260 fn test_execute_nested_function_calls() {
2261 let mut shell_state = ShellState::new();
2262
2263 shell_state.set_var("global_var", "global".to_string());
2265
2266 let outer_func = Ast::FunctionDefinition {
2268 name: "outer".to_string(),
2269 body: Box::new(Ast::Sequence(vec![
2270 Ast::Assignment {
2271 var: "global_var".to_string(),
2272 value: "outer_modified".to_string(),
2273 },
2274 Ast::FunctionCall {
2275 name: "inner".to_string(),
2276 args: vec![],
2277 },
2278 Ast::Pipeline(vec![ShellCommand {
2279 args: vec!["printf".to_string(), "outer_done".to_string()],
2280 redirections: Vec::new(),
2281 compound: None,
2282 }]),
2283 ])),
2284 };
2285
2286 let inner_func = Ast::FunctionDefinition {
2288 name: "inner".to_string(),
2289 body: Box::new(Ast::Sequence(vec![
2290 Ast::Assignment {
2291 var: "global_var".to_string(),
2292 value: "inner_modified".to_string(),
2293 },
2294 Ast::Pipeline(vec![ShellCommand {
2295 args: vec!["printf".to_string(), "inner_done".to_string()],
2296 redirections: Vec::new(),
2297 compound: None,
2298 }]),
2299 ])),
2300 };
2301
2302 execute(outer_func, &mut shell_state);
2304 execute(inner_func, &mut shell_state);
2305
2306 shell_state.set_var("global_var", "initial".to_string());
2308
2309 let call_ast = Ast::FunctionCall {
2311 name: "outer".to_string(),
2312 args: vec![],
2313 };
2314 let exit_code = execute(call_ast, &mut shell_state);
2315 assert_eq!(exit_code, 0);
2316
2317 assert_eq!(
2320 shell_state.get_var("global_var"),
2321 Some("inner_modified".to_string())
2322 );
2323 }
2324
2325 #[test]
2326 fn test_here_string_execution() {
2327 let cmd = ShellCommand {
2329 args: vec!["cat".to_string()],
2330 redirections: Vec::new(),
2331 compound: None,
2332 };
2334
2335 assert_eq!(cmd.args, vec!["cat"]);
2338 }
2340
2341 #[test]
2342 fn test_here_document_execution() {
2343 let cmd = ShellCommand {
2345 args: vec!["cat".to_string()],
2346 redirections: Vec::new(),
2347 compound: None,
2348 };
2350
2351 assert_eq!(cmd.args, vec!["cat"]);
2354 }
2356
2357 #[test]
2358 fn test_here_document_with_variable_expansion() {
2359 let mut shell_state = ShellState::new();
2361 shell_state.set_var("PWD", "/test/path".to_string());
2362
2363 let content = "Working dir: $PWD";
2365 let expanded = expand_variables_in_string(content, &mut shell_state);
2366
2367 assert_eq!(expanded, "Working dir: /test/path");
2368 }
2369
2370 #[test]
2371 fn test_here_document_with_command_substitution_builtin() {
2372 let mut shell_state = ShellState::new();
2374 shell_state.set_var("PWD", "/test/dir".to_string());
2375
2376 let content = "Current directory: `pwd`";
2378 let expanded = expand_variables_in_string(content, &mut shell_state);
2379
2380 assert!(expanded.contains("Current directory: "));
2382 }
2383
2384 #[test]
2389 fn test_fd_output_redirection() {
2390 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2391
2392 use std::time::{SystemTime, UNIX_EPOCH};
2394 let timestamp = SystemTime::now()
2395 .duration_since(UNIX_EPOCH)
2396 .unwrap()
2397 .as_nanos();
2398 let temp_file = format!("/tmp/rush_test_fd_out_{}.txt", timestamp);
2399
2400 let cmd = ShellCommand {
2402 args: vec![
2403 "sh".to_string(),
2404 "-c".to_string(),
2405 "echo error >&2".to_string(),
2406 ],
2407 redirections: vec![Redirection::FdOutput(2, temp_file.clone())],
2408 compound: None,
2409 };
2410
2411 let mut shell_state = ShellState::new();
2412 let exit_code = execute_single_command(&cmd, &mut shell_state);
2413 assert_eq!(exit_code, 0);
2414
2415 let content = std::fs::read_to_string(&temp_file).unwrap();
2417 assert_eq!(content.trim(), "error");
2418
2419 let _ = std::fs::remove_file(&temp_file);
2421 }
2422
2423 #[test]
2424 fn test_fd_input_redirection() {
2425 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2426
2427 use std::time::{SystemTime, UNIX_EPOCH};
2429 let timestamp = SystemTime::now()
2430 .duration_since(UNIX_EPOCH)
2431 .unwrap()
2432 .as_nanos();
2433 let temp_file = format!("/tmp/rush_test_fd_in_{}.txt", timestamp);
2434
2435 std::fs::write(&temp_file, "test input\n").unwrap();
2436 std::thread::sleep(std::time::Duration::from_millis(10));
2437
2438 let cmd = ShellCommand {
2441 args: vec!["cat".to_string()],
2442 compound: None,
2443 redirections: vec![
2444 Redirection::FdInput(3, temp_file.clone()),
2445 Redirection::Input(temp_file.clone()),
2446 ],
2447 };
2448
2449 let mut shell_state = ShellState::new();
2450 let exit_code = execute_single_command(&cmd, &mut shell_state);
2451 assert_eq!(exit_code, 0);
2452
2453 let _ = std::fs::remove_file(&temp_file);
2455 }
2456
2457 #[test]
2458 fn test_fd_append_redirection() {
2459 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2460
2461 use std::time::{SystemTime, UNIX_EPOCH};
2463 let timestamp = SystemTime::now()
2464 .duration_since(UNIX_EPOCH)
2465 .unwrap()
2466 .as_nanos();
2467 let temp_file = format!("/tmp/rush_test_fd_append_{}.txt", timestamp);
2468
2469 std::fs::write(&temp_file, "first line\n").unwrap();
2470 std::thread::sleep(std::time::Duration::from_millis(10));
2471
2472 let cmd = ShellCommand {
2474 args: vec![
2475 "sh".to_string(),
2476 "-c".to_string(),
2477 "echo second line >&2".to_string(),
2478 ],
2479 redirections: vec![Redirection::FdAppend(2, temp_file.clone())],
2480 compound: None,
2481 };
2482
2483 let mut shell_state = ShellState::new();
2484 let exit_code = execute_single_command(&cmd, &mut shell_state);
2485 assert_eq!(exit_code, 0);
2486
2487 let content = std::fs::read_to_string(&temp_file).unwrap();
2489 assert!(content.contains("first line"));
2490 assert!(content.contains("second line"));
2491
2492 let _ = std::fs::remove_file(&temp_file);
2494 }
2495
2496 #[test]
2497 fn test_fd_duplication_stderr_to_stdout() {
2498 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2499
2500 use std::time::{SystemTime, UNIX_EPOCH};
2502 let timestamp = SystemTime::now()
2503 .duration_since(UNIX_EPOCH)
2504 .unwrap()
2505 .as_nanos();
2506 let temp_file = format!("/tmp/rush_test_fd_dup_{}.txt", timestamp);
2507
2508 let cmd = ShellCommand {
2512 args: vec![
2513 "sh".to_string(),
2514 "-c".to_string(),
2515 "echo test; echo error >&2".to_string(),
2516 ],
2517 compound: None,
2518 redirections: vec![Redirection::Output(temp_file.clone())],
2519 };
2520
2521 let mut shell_state = ShellState::new();
2522 let exit_code = execute_single_command(&cmd, &mut shell_state);
2523 assert_eq!(exit_code, 0);
2524
2525 assert!(std::path::Path::new(&temp_file).exists());
2527 let content = std::fs::read_to_string(&temp_file).unwrap();
2528 assert!(content.contains("test"));
2529
2530 let _ = std::fs::remove_file(&temp_file);
2532 }
2533
2534 #[test]
2535 fn test_fd_close() {
2536 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2537
2538 let cmd = ShellCommand {
2540 args: vec!["sh".to_string(), "-c".to_string(), "echo test".to_string()],
2541 redirections: vec![Redirection::FdClose(2)],
2542 compound: None,
2543 };
2544
2545 let mut shell_state = ShellState::new();
2546 let exit_code = execute_single_command(&cmd, &mut shell_state);
2547 assert_eq!(exit_code, 0);
2548
2549 assert!(shell_state.fd_table.borrow().is_closed(2));
2551 }
2552
2553 #[test]
2554 fn test_fd_read_write() {
2555 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2556
2557 use std::time::{SystemTime, UNIX_EPOCH};
2559 let timestamp = SystemTime::now()
2560 .duration_since(UNIX_EPOCH)
2561 .unwrap()
2562 .as_nanos();
2563 let temp_file = format!("/tmp/rush_test_fd_rw_{}.txt", timestamp);
2564
2565 std::fs::write(&temp_file, "initial content\n").unwrap();
2566 std::thread::sleep(std::time::Duration::from_millis(10));
2567
2568 let cmd = ShellCommand {
2570 args: vec!["cat".to_string()],
2571 compound: None,
2572 redirections: vec![
2573 Redirection::FdInputOutput(3, temp_file.clone()),
2574 Redirection::Input(temp_file.clone()),
2575 ],
2576 };
2577
2578 let mut shell_state = ShellState::new();
2579 let exit_code = execute_single_command(&cmd, &mut shell_state);
2580 assert_eq!(exit_code, 0);
2581
2582 let _ = std::fs::remove_file(&temp_file);
2584 }
2585
2586 #[test]
2587 fn test_multiple_fd_redirections() {
2588 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2589
2590 use std::time::{SystemTime, UNIX_EPOCH};
2592 let timestamp = SystemTime::now()
2593 .duration_since(UNIX_EPOCH)
2594 .unwrap()
2595 .as_nanos();
2596 let out_file = format!("/tmp/rush_test_fd_multi_out_{}.txt", timestamp);
2597 let err_file = format!("/tmp/rush_test_fd_multi_err_{}.txt", timestamp);
2598
2599 let cmd = ShellCommand {
2601 args: vec![
2602 "sh".to_string(),
2603 "-c".to_string(),
2604 "echo stdout; echo stderr >&2".to_string(),
2605 ],
2606 redirections: vec![
2607 Redirection::FdOutput(2, err_file.clone()),
2608 Redirection::Output(out_file.clone()),
2609 ],
2610 compound: None,
2611 };
2612
2613 let mut shell_state = ShellState::new();
2614 let exit_code = execute_single_command(&cmd, &mut shell_state);
2615 assert_eq!(exit_code, 0);
2616
2617 assert!(std::path::Path::new(&out_file).exists());
2619 assert!(std::path::Path::new(&err_file).exists());
2620
2621 let out_content = std::fs::read_to_string(&out_file).unwrap();
2623 let err_content = std::fs::read_to_string(&err_file).unwrap();
2624 assert!(out_content.contains("stdout"));
2625 assert!(err_content.contains("stderr"));
2626
2627 let _ = std::fs::remove_file(&out_file);
2629 let _ = std::fs::remove_file(&err_file);
2630 }
2631
2632 #[test]
2633 fn test_fd_swap_pattern() {
2634 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2635
2636 use std::time::{SystemTime, UNIX_EPOCH};
2638 let timestamp = SystemTime::now()
2639 .duration_since(UNIX_EPOCH)
2640 .unwrap()
2641 .as_nanos();
2642 let temp_file = format!("/tmp/rush_test_fd_swap_{}.txt", timestamp);
2643
2644 let cmd = ShellCommand {
2647 args: vec!["sh".to_string(), "-c".to_string(), "echo test".to_string()],
2648 redirections: vec![
2649 Redirection::FdOutput(3, temp_file.clone()), Redirection::FdClose(3), Redirection::Output(temp_file.clone()), ],
2653 compound: None,
2654 };
2655
2656 let mut shell_state = ShellState::new();
2657 let exit_code = execute_single_command(&cmd, &mut shell_state);
2658 assert_eq!(exit_code, 0);
2659
2660 assert!(shell_state.fd_table.borrow().is_closed(3));
2662
2663 let _ = std::fs::remove_file(&temp_file);
2665 }
2666
2667 #[test]
2668 fn test_fd_redirection_with_pipes() {
2669 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2670
2671 use std::time::{SystemTime, UNIX_EPOCH};
2673 let timestamp = SystemTime::now()
2674 .duration_since(UNIX_EPOCH)
2675 .unwrap()
2676 .as_nanos();
2677 let temp_file = format!("/tmp/rush_test_fd_pipe_{}.txt", timestamp);
2678
2679 let commands = vec![
2682 ShellCommand {
2683 args: vec!["echo".to_string(), "piped output".to_string()],
2684 redirections: vec![],
2685 compound: None,
2686 },
2687 ShellCommand {
2688 args: vec!["cat".to_string()],
2689 compound: None,
2690 redirections: vec![Redirection::Output(temp_file.clone())],
2691 },
2692 ];
2693
2694 let mut shell_state = ShellState::new();
2695 let exit_code = execute_pipeline(&commands, &mut shell_state);
2696 assert_eq!(exit_code, 0);
2697
2698 let content = std::fs::read_to_string(&temp_file).unwrap();
2700 assert!(content.contains("piped output"));
2701
2702 let _ = std::fs::remove_file(&temp_file);
2704 }
2705
2706 #[test]
2707 fn test_fd_error_invalid_fd_number() {
2708 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2709
2710 use std::time::{SystemTime, UNIX_EPOCH};
2712 let timestamp = SystemTime::now()
2713 .duration_since(UNIX_EPOCH)
2714 .unwrap()
2715 .as_nanos();
2716 let temp_file = format!("/tmp/rush_test_fd_invalid_{}.txt", timestamp);
2717
2718 let cmd = ShellCommand {
2720 args: vec!["echo".to_string(), "test".to_string()],
2721 compound: None,
2722 redirections: vec![Redirection::FdOutput(1025, temp_file.clone())],
2723 };
2724
2725 let mut shell_state = ShellState::new();
2726 let exit_code = execute_single_command(&cmd, &mut shell_state);
2727
2728 assert_eq!(exit_code, 1);
2730
2731 let _ = std::fs::remove_file(&temp_file);
2733 }
2734
2735 #[test]
2736 fn test_fd_error_duplicate_closed_fd() {
2737 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2738
2739 let cmd = ShellCommand {
2741 args: vec!["echo".to_string(), "test".to_string()],
2742 compound: None,
2743 redirections: vec![
2744 Redirection::FdClose(3),
2745 Redirection::FdDuplicate(2, 3), ],
2747 };
2748
2749 let mut shell_state = ShellState::new();
2750 let exit_code = execute_single_command(&cmd, &mut shell_state);
2751
2752 assert_eq!(exit_code, 1);
2754 }
2755
2756 #[test]
2757 fn test_fd_error_file_permission() {
2758 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2759
2760 let cmd = ShellCommand {
2762 args: vec!["echo".to_string(), "test".to_string()],
2763 redirections: vec![Redirection::FdOutput(2, "/proc/version".to_string())],
2764 compound: None,
2765 };
2766
2767 let mut shell_state = ShellState::new();
2768 let exit_code = execute_single_command(&cmd, &mut shell_state);
2769
2770 assert_eq!(exit_code, 1);
2772 }
2773
2774 #[test]
2775 fn test_fd_redirection_order() {
2776 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2777
2778 use std::time::{SystemTime, UNIX_EPOCH};
2780 let timestamp = SystemTime::now()
2781 .duration_since(UNIX_EPOCH)
2782 .unwrap()
2783 .as_nanos();
2784 let file1 = format!("/tmp/rush_test_fd_order1_{}.txt", timestamp);
2785 let file2 = format!("/tmp/rush_test_fd_order2_{}.txt", timestamp);
2786
2787 let cmd = ShellCommand {
2790 args: vec!["echo".to_string(), "test".to_string()],
2791 compound: None,
2792 redirections: vec![
2793 Redirection::Output(file1.clone()),
2794 Redirection::Output(file2.clone()),
2795 ],
2796 };
2797
2798 let mut shell_state = ShellState::new();
2799 let exit_code = execute_single_command(&cmd, &mut shell_state);
2800 assert_eq!(exit_code, 0);
2801
2802 let content2 = std::fs::read_to_string(&file2).unwrap();
2804 assert!(content2.contains("test"));
2805
2806 let _ = std::fs::remove_file(&file1);
2808 let _ = std::fs::remove_file(&file2);
2809 }
2810
2811 #[test]
2812 fn test_fd_builtin_with_redirection() {
2813 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2814
2815 use std::time::{SystemTime, UNIX_EPOCH};
2817 let timestamp = SystemTime::now()
2818 .duration_since(UNIX_EPOCH)
2819 .unwrap()
2820 .as_nanos();
2821 let temp_file = format!("/tmp/rush_test_fd_builtin_{}.txt", timestamp);
2822
2823 let cmd = ShellCommand {
2825 args: vec!["echo".to_string(), "builtin test".to_string()],
2826 redirections: vec![Redirection::Output(temp_file.clone())],
2827 compound: None,
2828 };
2829
2830 let mut shell_state = ShellState::new();
2831 let exit_code = execute_single_command(&cmd, &mut shell_state);
2832 assert_eq!(exit_code, 0);
2833
2834 let content = std::fs::read_to_string(&temp_file).unwrap();
2836 assert!(content.contains("builtin test"));
2837
2838 let _ = std::fs::remove_file(&temp_file);
2840 }
2841
2842 #[test]
2843 fn test_fd_variable_expansion_in_filename() {
2844 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2845
2846 use std::time::{SystemTime, UNIX_EPOCH};
2848 let timestamp = SystemTime::now()
2849 .duration_since(UNIX_EPOCH)
2850 .unwrap()
2851 .as_nanos();
2852 let temp_file = format!("/tmp/rush_test_fd_var_{}.txt", timestamp);
2853
2854 let mut shell_state = ShellState::new();
2856 shell_state.set_var("OUTFILE", temp_file.clone());
2857
2858 let cmd = ShellCommand {
2860 args: vec!["echo".to_string(), "variable test".to_string()],
2861 compound: None,
2862 redirections: vec![Redirection::Output("$OUTFILE".to_string())],
2863 };
2864
2865 let exit_code = execute_single_command(&cmd, &mut shell_state);
2866 assert_eq!(exit_code, 0);
2867
2868 let content = std::fs::read_to_string(&temp_file).unwrap();
2870 assert!(content.contains("variable test"));
2871
2872 let _ = std::fs::remove_file(&temp_file);
2874 }
2875}