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 match shell_state.fd_table.borrow().deep_clone() {
1802 Ok(new_fd_table) => {
1803 subshell_state.fd_table = Rc::new(RefCell::new(new_fd_table));
1804 }
1805 Err(e) => {
1806 if shell_state.colors_enabled {
1807 eprintln!(
1808 "{}Failed to clone file descriptor table: {}\x1b[0m",
1809 shell_state.color_scheme.error, e
1810 );
1811 } else {
1812 eprintln!("Failed to clone file descriptor table: {}", e);
1813 }
1814 return 1;
1815 }
1816 }
1817
1818 subshell_state.subshell_depth = shell_state.subshell_depth + 1;
1820
1821 let parent_traps = shell_state.trap_handlers.lock().unwrap().clone();
1823 subshell_state.trap_handlers = std::sync::Arc::new(std::sync::Mutex::new(parent_traps));
1824
1825 let exit_code = execute(body, &mut subshell_state);
1827
1828 let final_exit_code = if subshell_state.exit_requested {
1831 subshell_state.exit_code
1833 } else if subshell_state.is_returning() {
1834 subshell_state.get_return_value().unwrap_or(exit_code)
1837 } else {
1838 exit_code
1839 };
1840
1841 subshell_state.fd_table.borrow_mut().clear();
1844
1845 if let Some(dir) = original_dir {
1847 let _ = std::env::set_current_dir(dir);
1848 }
1849
1850 shell_state.last_exit_code = final_exit_code;
1852
1853 final_exit_code
1855}
1856
1857fn execute_compound_with_redirections(
1867 compound_ast: &Ast,
1868 shell_state: &mut ShellState,
1869 redirections: &[Redirection],
1870) -> i32 {
1871 match compound_ast {
1872 Ast::Subshell { body } => {
1873 let has_output_redir = redirections.iter().any(|r| {
1880 matches!(
1881 r,
1882 Redirection::Output(_)
1883 | Redirection::Append(_)
1884 | Redirection::FdOutput(_, _)
1885 | Redirection::FdAppend(_, _)
1886 )
1887 });
1888
1889 if has_output_redir {
1890 let mut subshell_state = shell_state.clone();
1892
1893 let capture_buffer = Rc::new(RefCell::new(Vec::new()));
1895 subshell_state.capture_output = Some(capture_buffer.clone());
1896
1897 let exit_code = execute(*body.clone(), &mut subshell_state);
1899
1900 let output = capture_buffer.borrow().clone();
1902
1903 for redir in redirections {
1905 match redir {
1906 Redirection::Output(file) => {
1907 let expanded_file = expand_variables_in_string(file, shell_state);
1908 if let Err(e) = std::fs::write(&expanded_file, &output) {
1909 if shell_state.colors_enabled {
1910 eprintln!(
1911 "{}Redirection error: {}\x1b[0m",
1912 shell_state.color_scheme.error, e
1913 );
1914 } else {
1915 eprintln!("Redirection error: {}", e);
1916 }
1917 return 1;
1918 }
1919 }
1920 Redirection::Append(file) => {
1921 let expanded_file = expand_variables_in_string(file, shell_state);
1922 use std::fs::OpenOptions;
1923 let mut file_handle = match OpenOptions::new()
1924 .append(true)
1925 .create(true)
1926 .open(&expanded_file)
1927 {
1928 Ok(f) => f,
1929 Err(e) => {
1930 if shell_state.colors_enabled {
1931 eprintln!(
1932 "{}Redirection error: {}\x1b[0m",
1933 shell_state.color_scheme.error, e
1934 );
1935 } else {
1936 eprintln!("Redirection error: {}", e);
1937 }
1938 return 1;
1939 }
1940 };
1941 if let Err(e) = file_handle.write_all(&output) {
1942 if shell_state.colors_enabled {
1943 eprintln!(
1944 "{}Redirection error: {}\x1b[0m",
1945 shell_state.color_scheme.error, e
1946 );
1947 } else {
1948 eprintln!("Redirection error: {}", e);
1949 }
1950 return 1;
1951 }
1952 }
1953 _ => {
1954 }
1957 }
1958 }
1959
1960 shell_state.last_exit_code = exit_code;
1961 exit_code
1962 } else {
1963 execute_subshell(*body.clone(), shell_state)
1965 }
1966 }
1967 _ => {
1968 eprintln!("Unsupported compound command type");
1969 1
1970 }
1971 }
1972}
1973
1974fn execute_compound_in_pipeline(
1985 compound_ast: &Ast,
1986 shell_state: &mut ShellState,
1987 is_first: bool,
1988 is_last: bool,
1989 _redirections: &[Redirection],
1990) -> i32 {
1991 match compound_ast {
1992 Ast::Subshell { body } => {
1993 let mut subshell_state = shell_state.clone();
1995
1996 let _null_file = if !is_first {
2000 if let Ok(f) = File::open("/dev/null") {
2001 subshell_state.stdin_override = Some(f.as_raw_fd());
2002 Some(f)
2003 } else {
2004 None
2005 }
2006 } else {
2007 None
2008 };
2009
2010 let exit_code = if !is_last || shell_state.capture_output.is_some() {
2012 let capture_buffer = Rc::new(RefCell::new(Vec::new()));
2014 subshell_state.capture_output = Some(capture_buffer.clone());
2015
2016 let code = execute(*body.clone(), &mut subshell_state);
2018
2019 if let Some(ref parent_capture) = shell_state.capture_output {
2021 let captured = capture_buffer.borrow().clone();
2022 parent_capture.borrow_mut().extend_from_slice(&captured);
2023 }
2024
2025 shell_state.last_exit_code = code;
2027
2028 code
2029 } else {
2030 let code = execute(*body.clone(), &mut subshell_state);
2032 shell_state.last_exit_code = code;
2033 code
2034 };
2035
2036 exit_code
2037 }
2038 _ => {
2039 eprintln!("Unsupported compound command in pipeline");
2041 1
2042 }
2043 }
2044}
2045
2046#[cfg(test)]
2047mod tests {
2048 use super::*;
2049 use std::sync::Mutex;
2050
2051 static ENV_LOCK: Mutex<()> = Mutex::new(());
2053
2054 #[test]
2055 fn test_execute_single_command_builtin() {
2056 let cmd = ShellCommand {
2057 args: vec!["true".to_string()],
2058 redirections: Vec::new(),
2059 compound: None,
2060 };
2061 let mut shell_state = ShellState::new();
2062 let exit_code = execute_single_command(&cmd, &mut shell_state);
2063 assert_eq!(exit_code, 0);
2064 }
2065
2066 #[test]
2068 fn test_execute_single_command_external() {
2069 let cmd = ShellCommand {
2070 args: vec!["true".to_string()], redirections: Vec::new(),
2072 compound: None,
2073 };
2074 let mut shell_state = ShellState::new();
2075 let exit_code = execute_single_command(&cmd, &mut shell_state);
2076 assert_eq!(exit_code, 0);
2077 }
2078
2079 #[test]
2080 fn test_execute_single_command_external_nonexistent() {
2081 let cmd = ShellCommand {
2082 args: vec!["nonexistent_command".to_string()],
2083 redirections: Vec::new(),
2084 compound: None,
2085 };
2086 let mut shell_state = ShellState::new();
2087 let exit_code = execute_single_command(&cmd, &mut shell_state);
2088 assert_eq!(exit_code, 1); }
2090
2091 #[test]
2092 fn test_execute_pipeline() {
2093 let commands = vec![
2094 ShellCommand {
2095 args: vec!["printf".to_string(), "hello".to_string()],
2096 redirections: Vec::new(),
2097 compound: None,
2098 },
2099 ShellCommand {
2100 args: vec!["cat".to_string()], redirections: Vec::new(),
2102 compound: None,
2103 },
2104 ];
2105 let mut shell_state = ShellState::new();
2106 let exit_code = execute_pipeline(&commands, &mut shell_state);
2107 assert_eq!(exit_code, 0);
2108 }
2109
2110 #[test]
2111 fn test_execute_empty_pipeline() {
2112 let commands = vec![];
2113 let mut shell_state = ShellState::new();
2114 let exit_code = execute(Ast::Pipeline(commands), &mut shell_state);
2115 assert_eq!(exit_code, 0);
2116 }
2117
2118 #[test]
2119 fn test_execute_single_command() {
2120 let ast = Ast::Pipeline(vec![ShellCommand {
2121 args: vec!["true".to_string()],
2122 redirections: Vec::new(),
2123 compound: None,
2124 }]);
2125 let mut shell_state = ShellState::new();
2126 let exit_code = execute(ast, &mut shell_state);
2127 assert_eq!(exit_code, 0);
2128 }
2129
2130 #[test]
2131 fn test_execute_function_definition() {
2132 let ast = Ast::FunctionDefinition {
2133 name: "test_func".to_string(),
2134 body: Box::new(Ast::Pipeline(vec![ShellCommand {
2135 args: vec!["echo".to_string(), "hello".to_string()],
2136 redirections: Vec::new(),
2137 compound: None,
2138 }])),
2139 };
2140 let mut shell_state = ShellState::new();
2141 let exit_code = execute(ast, &mut shell_state);
2142 assert_eq!(exit_code, 0);
2143
2144 assert!(shell_state.get_function("test_func").is_some());
2146 }
2147
2148 #[test]
2149 fn test_execute_function_call() {
2150 let mut shell_state = ShellState::new();
2152 shell_state.define_function(
2153 "test_func".to_string(),
2154 Ast::Pipeline(vec![ShellCommand {
2155 args: vec!["echo".to_string(), "hello".to_string()],
2156 redirections: Vec::new(),
2157 compound: None,
2158 }]),
2159 );
2160
2161 let ast = Ast::FunctionCall {
2163 name: "test_func".to_string(),
2164 args: vec![],
2165 };
2166 let exit_code = execute(ast, &mut shell_state);
2167 assert_eq!(exit_code, 0);
2168 }
2169
2170 #[test]
2171 fn test_execute_function_call_with_args() {
2172 let mut shell_state = ShellState::new();
2174 shell_state.define_function(
2175 "test_func".to_string(),
2176 Ast::Pipeline(vec![ShellCommand {
2177 args: vec!["echo".to_string(), "arg1".to_string()],
2178 redirections: Vec::new(),
2179 compound: None,
2180 }]),
2181 );
2182
2183 let ast = Ast::FunctionCall {
2185 name: "test_func".to_string(),
2186 args: vec!["hello".to_string()],
2187 };
2188 let exit_code = execute(ast, &mut shell_state);
2189 assert_eq!(exit_code, 0);
2190 }
2191
2192 #[test]
2193 fn test_execute_nonexistent_function() {
2194 let mut shell_state = ShellState::new();
2195 let ast = Ast::FunctionCall {
2196 name: "nonexistent".to_string(),
2197 args: vec![],
2198 };
2199 let exit_code = execute(ast, &mut shell_state);
2200 assert_eq!(exit_code, 1); }
2202
2203 #[test]
2204 fn test_execute_function_integration() {
2205 let mut shell_state = ShellState::new();
2207
2208 let define_ast = Ast::FunctionDefinition {
2210 name: "hello".to_string(),
2211 body: Box::new(Ast::Pipeline(vec![ShellCommand {
2212 args: vec!["printf".to_string(), "Hello from function".to_string()],
2213 redirections: Vec::new(),
2214 compound: None,
2215 }])),
2216 };
2217 let exit_code = execute(define_ast, &mut shell_state);
2218 assert_eq!(exit_code, 0);
2219
2220 let call_ast = Ast::FunctionCall {
2222 name: "hello".to_string(),
2223 args: vec![],
2224 };
2225 let exit_code = execute(call_ast, &mut shell_state);
2226 assert_eq!(exit_code, 0);
2227 }
2228
2229 #[test]
2230 fn test_execute_function_with_local_variables() {
2231 let mut shell_state = ShellState::new();
2232
2233 shell_state.set_var("global_var", "global_value".to_string());
2235
2236 let define_ast = Ast::FunctionDefinition {
2238 name: "test_func".to_string(),
2239 body: Box::new(Ast::Sequence(vec![
2240 Ast::LocalAssignment {
2241 var: "local_var".to_string(),
2242 value: "local_value".to_string(),
2243 },
2244 Ast::Assignment {
2245 var: "global_var".to_string(),
2246 value: "modified_in_function".to_string(),
2247 },
2248 Ast::Pipeline(vec![ShellCommand {
2249 args: vec!["printf".to_string(), "success".to_string()],
2250 redirections: Vec::new(),
2251 compound: None,
2252 }]),
2253 ])),
2254 };
2255 let exit_code = execute(define_ast, &mut shell_state);
2256 assert_eq!(exit_code, 0);
2257
2258 assert_eq!(
2260 shell_state.get_var("global_var"),
2261 Some("global_value".to_string())
2262 );
2263
2264 let call_ast = Ast::FunctionCall {
2266 name: "test_func".to_string(),
2267 args: vec![],
2268 };
2269 let exit_code = execute(call_ast, &mut shell_state);
2270 assert_eq!(exit_code, 0);
2271
2272 assert_eq!(
2274 shell_state.get_var("global_var"),
2275 Some("modified_in_function".to_string())
2276 );
2277 }
2278
2279 #[test]
2280 fn test_execute_nested_function_calls() {
2281 let mut shell_state = ShellState::new();
2282
2283 shell_state.set_var("global_var", "global".to_string());
2285
2286 let outer_func = Ast::FunctionDefinition {
2288 name: "outer".to_string(),
2289 body: Box::new(Ast::Sequence(vec![
2290 Ast::Assignment {
2291 var: "global_var".to_string(),
2292 value: "outer_modified".to_string(),
2293 },
2294 Ast::FunctionCall {
2295 name: "inner".to_string(),
2296 args: vec![],
2297 },
2298 Ast::Pipeline(vec![ShellCommand {
2299 args: vec!["printf".to_string(), "outer_done".to_string()],
2300 redirections: Vec::new(),
2301 compound: None,
2302 }]),
2303 ])),
2304 };
2305
2306 let inner_func = Ast::FunctionDefinition {
2308 name: "inner".to_string(),
2309 body: Box::new(Ast::Sequence(vec![
2310 Ast::Assignment {
2311 var: "global_var".to_string(),
2312 value: "inner_modified".to_string(),
2313 },
2314 Ast::Pipeline(vec![ShellCommand {
2315 args: vec!["printf".to_string(), "inner_done".to_string()],
2316 redirections: Vec::new(),
2317 compound: None,
2318 }]),
2319 ])),
2320 };
2321
2322 execute(outer_func, &mut shell_state);
2324 execute(inner_func, &mut shell_state);
2325
2326 shell_state.set_var("global_var", "initial".to_string());
2328
2329 let call_ast = Ast::FunctionCall {
2331 name: "outer".to_string(),
2332 args: vec![],
2333 };
2334 let exit_code = execute(call_ast, &mut shell_state);
2335 assert_eq!(exit_code, 0);
2336
2337 assert_eq!(
2340 shell_state.get_var("global_var"),
2341 Some("inner_modified".to_string())
2342 );
2343 }
2344
2345 #[test]
2346 fn test_here_string_execution() {
2347 let cmd = ShellCommand {
2349 args: vec!["cat".to_string()],
2350 redirections: Vec::new(),
2351 compound: None,
2352 };
2354
2355 assert_eq!(cmd.args, vec!["cat"]);
2358 }
2360
2361 #[test]
2362 fn test_here_document_execution() {
2363 let cmd = ShellCommand {
2365 args: vec!["cat".to_string()],
2366 redirections: Vec::new(),
2367 compound: None,
2368 };
2370
2371 assert_eq!(cmd.args, vec!["cat"]);
2374 }
2376
2377 #[test]
2378 fn test_here_document_with_variable_expansion() {
2379 let mut shell_state = ShellState::new();
2381 shell_state.set_var("PWD", "/test/path".to_string());
2382
2383 let content = "Working dir: $PWD";
2385 let expanded = expand_variables_in_string(content, &mut shell_state);
2386
2387 assert_eq!(expanded, "Working dir: /test/path");
2388 }
2389
2390 #[test]
2391 fn test_here_document_with_command_substitution_builtin() {
2392 let mut shell_state = ShellState::new();
2394 shell_state.set_var("PWD", "/test/dir".to_string());
2395
2396 let content = "Current directory: `pwd`";
2398 let expanded = expand_variables_in_string(content, &mut shell_state);
2399
2400 assert!(expanded.contains("Current directory: "));
2402 }
2403
2404 #[test]
2409 fn test_fd_output_redirection() {
2410 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2411
2412 use std::time::{SystemTime, UNIX_EPOCH};
2414 let timestamp = SystemTime::now()
2415 .duration_since(UNIX_EPOCH)
2416 .unwrap()
2417 .as_nanos();
2418 let temp_file = format!("/tmp/rush_test_fd_out_{}.txt", timestamp);
2419
2420 let cmd = ShellCommand {
2422 args: vec![
2423 "sh".to_string(),
2424 "-c".to_string(),
2425 "echo error >&2".to_string(),
2426 ],
2427 redirections: vec![Redirection::FdOutput(2, temp_file.clone())],
2428 compound: None,
2429 };
2430
2431 let mut shell_state = ShellState::new();
2432 let exit_code = execute_single_command(&cmd, &mut shell_state);
2433 assert_eq!(exit_code, 0);
2434
2435 let content = std::fs::read_to_string(&temp_file).unwrap();
2437 assert_eq!(content.trim(), "error");
2438
2439 let _ = std::fs::remove_file(&temp_file);
2441 }
2442
2443 #[test]
2444 fn test_fd_input_redirection() {
2445 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2446
2447 use std::time::{SystemTime, UNIX_EPOCH};
2449 let timestamp = SystemTime::now()
2450 .duration_since(UNIX_EPOCH)
2451 .unwrap()
2452 .as_nanos();
2453 let temp_file = format!("/tmp/rush_test_fd_in_{}.txt", timestamp);
2454
2455 std::fs::write(&temp_file, "test input\n").unwrap();
2456 std::thread::sleep(std::time::Duration::from_millis(10));
2457
2458 let cmd = ShellCommand {
2461 args: vec!["cat".to_string()],
2462 compound: None,
2463 redirections: vec![
2464 Redirection::FdInput(3, temp_file.clone()),
2465 Redirection::Input(temp_file.clone()),
2466 ],
2467 };
2468
2469 let mut shell_state = ShellState::new();
2470 let exit_code = execute_single_command(&cmd, &mut shell_state);
2471 assert_eq!(exit_code, 0);
2472
2473 let _ = std::fs::remove_file(&temp_file);
2475 }
2476
2477 #[test]
2478 fn test_fd_append_redirection() {
2479 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2480
2481 use std::time::{SystemTime, UNIX_EPOCH};
2483 let timestamp = SystemTime::now()
2484 .duration_since(UNIX_EPOCH)
2485 .unwrap()
2486 .as_nanos();
2487 let temp_file = format!("/tmp/rush_test_fd_append_{}.txt", timestamp);
2488
2489 std::fs::write(&temp_file, "first line\n").unwrap();
2490 std::thread::sleep(std::time::Duration::from_millis(10));
2491
2492 let cmd = ShellCommand {
2494 args: vec![
2495 "sh".to_string(),
2496 "-c".to_string(),
2497 "echo second line >&2".to_string(),
2498 ],
2499 redirections: vec![Redirection::FdAppend(2, temp_file.clone())],
2500 compound: None,
2501 };
2502
2503 let mut shell_state = ShellState::new();
2504 let exit_code = execute_single_command(&cmd, &mut shell_state);
2505 assert_eq!(exit_code, 0);
2506
2507 let content = std::fs::read_to_string(&temp_file).unwrap();
2509 assert!(content.contains("first line"));
2510 assert!(content.contains("second line"));
2511
2512 let _ = std::fs::remove_file(&temp_file);
2514 }
2515
2516 #[test]
2517 fn test_fd_duplication_stderr_to_stdout() {
2518 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2519
2520 use std::time::{SystemTime, UNIX_EPOCH};
2522 let timestamp = SystemTime::now()
2523 .duration_since(UNIX_EPOCH)
2524 .unwrap()
2525 .as_nanos();
2526 let temp_file = format!("/tmp/rush_test_fd_dup_{}.txt", timestamp);
2527
2528 let cmd = ShellCommand {
2532 args: vec![
2533 "sh".to_string(),
2534 "-c".to_string(),
2535 "echo test; echo error >&2".to_string(),
2536 ],
2537 compound: None,
2538 redirections: vec![Redirection::Output(temp_file.clone())],
2539 };
2540
2541 let mut shell_state = ShellState::new();
2542 let exit_code = execute_single_command(&cmd, &mut shell_state);
2543 assert_eq!(exit_code, 0);
2544
2545 assert!(std::path::Path::new(&temp_file).exists());
2547 let content = std::fs::read_to_string(&temp_file).unwrap();
2548 assert!(content.contains("test"));
2549
2550 let _ = std::fs::remove_file(&temp_file);
2552 }
2553
2554 #[test]
2555 fn test_fd_close() {
2556 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2557
2558 let cmd = ShellCommand {
2560 args: vec!["sh".to_string(), "-c".to_string(), "echo test".to_string()],
2561 redirections: vec![Redirection::FdClose(2)],
2562 compound: None,
2563 };
2564
2565 let mut shell_state = ShellState::new();
2566 let exit_code = execute_single_command(&cmd, &mut shell_state);
2567 assert_eq!(exit_code, 0);
2568
2569 assert!(shell_state.fd_table.borrow().is_closed(2));
2571 }
2572
2573 #[test]
2574 fn test_fd_read_write() {
2575 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2576
2577 use std::time::{SystemTime, UNIX_EPOCH};
2579 let timestamp = SystemTime::now()
2580 .duration_since(UNIX_EPOCH)
2581 .unwrap()
2582 .as_nanos();
2583 let temp_file = format!("/tmp/rush_test_fd_rw_{}.txt", timestamp);
2584
2585 std::fs::write(&temp_file, "initial content\n").unwrap();
2586 std::thread::sleep(std::time::Duration::from_millis(10));
2587
2588 let cmd = ShellCommand {
2590 args: vec!["cat".to_string()],
2591 compound: None,
2592 redirections: vec![
2593 Redirection::FdInputOutput(3, temp_file.clone()),
2594 Redirection::Input(temp_file.clone()),
2595 ],
2596 };
2597
2598 let mut shell_state = ShellState::new();
2599 let exit_code = execute_single_command(&cmd, &mut shell_state);
2600 assert_eq!(exit_code, 0);
2601
2602 let _ = std::fs::remove_file(&temp_file);
2604 }
2605
2606 #[test]
2607 fn test_multiple_fd_redirections() {
2608 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2609
2610 use std::time::{SystemTime, UNIX_EPOCH};
2612 let timestamp = SystemTime::now()
2613 .duration_since(UNIX_EPOCH)
2614 .unwrap()
2615 .as_nanos();
2616 let out_file = format!("/tmp/rush_test_fd_multi_out_{}.txt", timestamp);
2617 let err_file = format!("/tmp/rush_test_fd_multi_err_{}.txt", timestamp);
2618
2619 let cmd = ShellCommand {
2621 args: vec![
2622 "sh".to_string(),
2623 "-c".to_string(),
2624 "echo stdout; echo stderr >&2".to_string(),
2625 ],
2626 redirections: vec![
2627 Redirection::FdOutput(2, err_file.clone()),
2628 Redirection::Output(out_file.clone()),
2629 ],
2630 compound: None,
2631 };
2632
2633 let mut shell_state = ShellState::new();
2634 let exit_code = execute_single_command(&cmd, &mut shell_state);
2635 assert_eq!(exit_code, 0);
2636
2637 assert!(std::path::Path::new(&out_file).exists());
2639 assert!(std::path::Path::new(&err_file).exists());
2640
2641 let out_content = std::fs::read_to_string(&out_file).unwrap();
2643 let err_content = std::fs::read_to_string(&err_file).unwrap();
2644 assert!(out_content.contains("stdout"));
2645 assert!(err_content.contains("stderr"));
2646
2647 let _ = std::fs::remove_file(&out_file);
2649 let _ = std::fs::remove_file(&err_file);
2650 }
2651
2652 #[test]
2653 fn test_fd_swap_pattern() {
2654 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2655
2656 use std::time::{SystemTime, UNIX_EPOCH};
2658 let timestamp = SystemTime::now()
2659 .duration_since(UNIX_EPOCH)
2660 .unwrap()
2661 .as_nanos();
2662 let temp_file = format!("/tmp/rush_test_fd_swap_{}.txt", timestamp);
2663
2664 let cmd = ShellCommand {
2667 args: vec!["sh".to_string(), "-c".to_string(), "echo test".to_string()],
2668 redirections: vec![
2669 Redirection::FdOutput(3, temp_file.clone()), Redirection::FdClose(3), Redirection::Output(temp_file.clone()), ],
2673 compound: None,
2674 };
2675
2676 let mut shell_state = ShellState::new();
2677 let exit_code = execute_single_command(&cmd, &mut shell_state);
2678 assert_eq!(exit_code, 0);
2679
2680 assert!(shell_state.fd_table.borrow().is_closed(3));
2682
2683 let _ = std::fs::remove_file(&temp_file);
2685 }
2686
2687 #[test]
2688 fn test_fd_redirection_with_pipes() {
2689 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2690
2691 use std::time::{SystemTime, UNIX_EPOCH};
2693 let timestamp = SystemTime::now()
2694 .duration_since(UNIX_EPOCH)
2695 .unwrap()
2696 .as_nanos();
2697 let temp_file = format!("/tmp/rush_test_fd_pipe_{}.txt", timestamp);
2698
2699 let commands = vec![
2702 ShellCommand {
2703 args: vec!["echo".to_string(), "piped output".to_string()],
2704 redirections: vec![],
2705 compound: None,
2706 },
2707 ShellCommand {
2708 args: vec!["cat".to_string()],
2709 compound: None,
2710 redirections: vec![Redirection::Output(temp_file.clone())],
2711 },
2712 ];
2713
2714 let mut shell_state = ShellState::new();
2715 let exit_code = execute_pipeline(&commands, &mut shell_state);
2716 assert_eq!(exit_code, 0);
2717
2718 let content = std::fs::read_to_string(&temp_file).unwrap();
2720 assert!(content.contains("piped output"));
2721
2722 let _ = std::fs::remove_file(&temp_file);
2724 }
2725
2726 #[test]
2727 fn test_fd_error_invalid_fd_number() {
2728 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2729
2730 use std::time::{SystemTime, UNIX_EPOCH};
2732 let timestamp = SystemTime::now()
2733 .duration_since(UNIX_EPOCH)
2734 .unwrap()
2735 .as_nanos();
2736 let temp_file = format!("/tmp/rush_test_fd_invalid_{}.txt", timestamp);
2737
2738 let cmd = ShellCommand {
2740 args: vec!["echo".to_string(), "test".to_string()],
2741 compound: None,
2742 redirections: vec![Redirection::FdOutput(1025, temp_file.clone())],
2743 };
2744
2745 let mut shell_state = ShellState::new();
2746 let exit_code = execute_single_command(&cmd, &mut shell_state);
2747
2748 assert_eq!(exit_code, 1);
2750
2751 let _ = std::fs::remove_file(&temp_file);
2753 }
2754
2755 #[test]
2756 fn test_fd_error_duplicate_closed_fd() {
2757 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2758
2759 let cmd = ShellCommand {
2761 args: vec!["echo".to_string(), "test".to_string()],
2762 compound: None,
2763 redirections: vec![
2764 Redirection::FdClose(3),
2765 Redirection::FdDuplicate(2, 3), ],
2767 };
2768
2769 let mut shell_state = ShellState::new();
2770 let exit_code = execute_single_command(&cmd, &mut shell_state);
2771
2772 assert_eq!(exit_code, 1);
2774 }
2775
2776 #[test]
2777 fn test_fd_error_file_permission() {
2778 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2779
2780 let cmd = ShellCommand {
2782 args: vec!["echo".to_string(), "test".to_string()],
2783 redirections: vec![Redirection::FdOutput(2, "/proc/version".to_string())],
2784 compound: None,
2785 };
2786
2787 let mut shell_state = ShellState::new();
2788 let exit_code = execute_single_command(&cmd, &mut shell_state);
2789
2790 assert_eq!(exit_code, 1);
2792 }
2793
2794 #[test]
2795 fn test_fd_redirection_order() {
2796 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2797
2798 use std::time::{SystemTime, UNIX_EPOCH};
2800 let timestamp = SystemTime::now()
2801 .duration_since(UNIX_EPOCH)
2802 .unwrap()
2803 .as_nanos();
2804 let file1 = format!("/tmp/rush_test_fd_order1_{}.txt", timestamp);
2805 let file2 = format!("/tmp/rush_test_fd_order2_{}.txt", timestamp);
2806
2807 let cmd = ShellCommand {
2810 args: vec!["echo".to_string(), "test".to_string()],
2811 compound: None,
2812 redirections: vec![
2813 Redirection::Output(file1.clone()),
2814 Redirection::Output(file2.clone()),
2815 ],
2816 };
2817
2818 let mut shell_state = ShellState::new();
2819 let exit_code = execute_single_command(&cmd, &mut shell_state);
2820 assert_eq!(exit_code, 0);
2821
2822 let content2 = std::fs::read_to_string(&file2).unwrap();
2824 assert!(content2.contains("test"));
2825
2826 let _ = std::fs::remove_file(&file1);
2828 let _ = std::fs::remove_file(&file2);
2829 }
2830
2831 #[test]
2832 fn test_fd_builtin_with_redirection() {
2833 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2834
2835 use std::time::{SystemTime, UNIX_EPOCH};
2837 let timestamp = SystemTime::now()
2838 .duration_since(UNIX_EPOCH)
2839 .unwrap()
2840 .as_nanos();
2841 let temp_file = format!("/tmp/rush_test_fd_builtin_{}.txt", timestamp);
2842
2843 let cmd = ShellCommand {
2845 args: vec!["echo".to_string(), "builtin test".to_string()],
2846 redirections: vec![Redirection::Output(temp_file.clone())],
2847 compound: None,
2848 };
2849
2850 let mut shell_state = ShellState::new();
2851 let exit_code = execute_single_command(&cmd, &mut shell_state);
2852 assert_eq!(exit_code, 0);
2853
2854 let content = std::fs::read_to_string(&temp_file).unwrap();
2856 assert!(content.contains("builtin test"));
2857
2858 let _ = std::fs::remove_file(&temp_file);
2860 }
2861
2862 #[test]
2863 fn test_fd_variable_expansion_in_filename() {
2864 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2865
2866 use std::time::{SystemTime, UNIX_EPOCH};
2868 let timestamp = SystemTime::now()
2869 .duration_since(UNIX_EPOCH)
2870 .unwrap()
2871 .as_nanos();
2872 let temp_file = format!("/tmp/rush_test_fd_var_{}.txt", timestamp);
2873
2874 let mut shell_state = ShellState::new();
2876 shell_state.set_var("OUTFILE", temp_file.clone());
2877
2878 let cmd = ShellCommand {
2880 args: vec!["echo".to_string(), "variable test".to_string()],
2881 compound: None,
2882 redirections: vec![Redirection::Output("$OUTFILE".to_string())],
2883 };
2884
2885 let exit_code = execute_single_command(&cmd, &mut shell_state);
2886 assert_eq!(exit_code, 0);
2887
2888 let content = std::fs::read_to_string(&temp_file).unwrap();
2890 assert!(content.contains("variable test"));
2891
2892 let _ = std::fs::remove_file(&temp_file);
2894 }
2895}