1use std::cell::RefCell;
2use std::fs::{File, OpenOptions};
3use std::io::{BufRead, BufReader, Write, pipe};
4use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd, 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> {
28 let (reader, writer) = pipe().map_err(|e| format!("Failed to create pipe: {}", e))?;
30
31 match &ast {
36 Ast::Pipeline(commands) => {
37 if commands.is_empty() {
39 return Ok(String::new());
40 }
41
42 if commands.len() == 1 {
43 let cmd = &commands[0];
45 if cmd.args.is_empty() {
46 return Ok(String::new());
47 }
48
49 let var_expanded_args = expand_variables_in_args(&cmd.args, shell_state);
51 let expanded_args = expand_wildcards(&var_expanded_args, shell_state)
52 .map_err(|e| format!("Wildcard expansion failed: {}", e))?;
53
54 if expanded_args.is_empty() {
55 return Ok(String::new());
56 }
57
58 if shell_state.get_function(&expanded_args[0]).is_some() {
60 let previous_capture = shell_state.capture_output.clone();
62
63 let capture_buffer = Rc::new(RefCell::new(Vec::new()));
65 shell_state.capture_output = Some(capture_buffer.clone());
66
67 let function_call_ast = Ast::FunctionCall {
69 name: expanded_args[0].clone(),
70 args: expanded_args[1..].to_vec(),
71 };
72
73 let exit_code = execute(function_call_ast, shell_state);
74
75 let captured = capture_buffer.borrow().clone();
77 let output = String::from_utf8_lossy(&captured).trim_end().to_string();
78
79 shell_state.capture_output = previous_capture;
81
82 if exit_code == 0 {
83 Ok(output)
84 } else {
85 Err(format!("Function failed with exit code {}", exit_code))
86 }
87 } else if crate::builtins::is_builtin(&expanded_args[0]) {
88 let temp_cmd = ShellCommand {
89 args: expanded_args,
90 redirections: cmd.redirections.clone(),
91 compound: None,
92 };
93
94 let exit_code = crate::builtins::execute_builtin(
96 &temp_cmd,
97 shell_state,
98 Some(Box::new(writer)),
99 );
100
101 drop(temp_cmd); let mut output = String::new();
104 use std::io::Read;
105 let mut reader = reader;
106 reader
107 .read_to_string(&mut output)
108 .map_err(|e| format!("Failed to read output: {}", e))?;
109
110 if exit_code == 0 {
111 Ok(output.trim_end().to_string())
112 } else {
113 Err(format!("Command failed with exit code {}", exit_code))
114 }
115 } else {
116 drop(writer); let mut command = Command::new(&expanded_args[0]);
120 command.args(&expanded_args[1..]);
121 command.stdout(Stdio::piped());
122 command.stderr(Stdio::null()); let child_env = shell_state.get_env_for_child();
126 command.env_clear();
127 for (key, value) in child_env {
128 command.env(key, value);
129 }
130
131 let output = command
132 .output()
133 .map_err(|e| format!("Failed to execute command: {}", e))?;
134
135 if output.status.success() {
136 Ok(String::from_utf8_lossy(&output.stdout)
137 .trim_end()
138 .to_string())
139 } else {
140 Err(format!(
141 "Command failed with exit code {}",
142 output.status.code().unwrap_or(1)
143 ))
144 }
145 }
146 } else {
147 drop(writer); let previous_capture = shell_state.capture_output.clone();
152
153 let capture_buffer = Rc::new(RefCell::new(Vec::new()));
155 shell_state.capture_output = Some(capture_buffer.clone());
156
157 let exit_code = execute_pipeline(commands, shell_state);
159
160 let captured = capture_buffer.borrow().clone();
162 let output = String::from_utf8_lossy(&captured).trim_end().to_string();
163
164 shell_state.capture_output = previous_capture;
166
167 if exit_code == 0 {
168 Ok(output)
169 } else {
170 Err(format!("Pipeline failed with exit code {}", exit_code))
171 }
172 }
173 }
174 _ => {
175 drop(writer);
177
178 let previous_capture = shell_state.capture_output.clone();
180
181 let capture_buffer = Rc::new(RefCell::new(Vec::new()));
183 shell_state.capture_output = Some(capture_buffer.clone());
184
185 let exit_code = execute(ast, shell_state);
187
188 let captured = capture_buffer.borrow().clone();
190 let output = String::from_utf8_lossy(&captured).trim_end().to_string();
191
192 shell_state.capture_output = previous_capture;
194
195 if exit_code == 0 {
196 Ok(output)
197 } else {
198 Err(format!("Command failed with exit code {}", exit_code))
199 }
200 }
201 }
202}
203
204fn expand_variables_in_args(args: &[String], shell_state: &mut ShellState) -> Vec<String> {
205 let mut expanded_args = Vec::new();
206
207 for arg in args {
208 let expanded_arg = expand_variables_in_string(arg, shell_state);
210 expanded_args.push(expanded_arg);
211 }
212
213 expanded_args
214}
215
216pub fn expand_variables_in_string(input: &str, shell_state: &mut ShellState) -> String {
233 let mut result = String::new();
234 let mut chars = input.chars().peekable();
235
236 while let Some(ch) = chars.next() {
237 if ch == '$' {
238 if let Some(&'(') = chars.peek() {
240 chars.next(); if let Some(&'(') = chars.peek() {
244 chars.next(); let mut arithmetic_expr = String::new();
247 let mut paren_depth = 1;
248 let mut found_closing = false;
249
250 while let Some(c) = chars.next() {
251 if c == '(' {
252 paren_depth += 1;
253 arithmetic_expr.push(c);
254 } else if c == ')' {
255 paren_depth -= 1;
256 if paren_depth == 0 {
257 if let Some(&')') = chars.peek() {
259 chars.next(); found_closing = true;
261 break;
262 } else {
263 result.push_str("$((");
265 result.push_str(&arithmetic_expr);
266 result.push(')');
267 break;
268 }
269 }
270 arithmetic_expr.push(c);
271 } else {
272 arithmetic_expr.push(c);
273 }
274 }
275
276 if found_closing {
277 let mut expanded_expr = String::new();
281 let mut expr_chars = arithmetic_expr.chars().peekable();
282
283 while let Some(ch) = expr_chars.next() {
284 if ch == '$' {
285 let mut var_name = String::new();
287 if let Some(&c) = expr_chars.peek() {
288 if c == '?'
289 || c == '$'
290 || c == '0'
291 || c == '#'
292 || c == '*'
293 || c == '@'
294 || c.is_ascii_digit()
295 {
296 var_name.push(c);
297 expr_chars.next();
298 } else {
299 while let Some(&c) = expr_chars.peek() {
300 if c.is_alphanumeric() || c == '_' {
301 var_name.push(c);
302 expr_chars.next();
303 } else {
304 break;
305 }
306 }
307 }
308 }
309
310 if !var_name.is_empty() {
311 if let Some(value) = shell_state.get_var(&var_name) {
312 expanded_expr.push_str(&value);
313 } else {
314 expanded_expr.push('0');
316 }
317 } else {
318 expanded_expr.push('$');
319 }
320 } else {
321 expanded_expr.push(ch);
322 }
323 }
324
325 match crate::arithmetic::evaluate_arithmetic_expression(
326 &expanded_expr,
327 shell_state,
328 ) {
329 Ok(value) => {
330 result.push_str(&value.to_string());
331 }
332 Err(e) => {
333 if shell_state.colors_enabled {
335 result.push_str(&format!(
336 "{}arithmetic error: {}{}",
337 shell_state.color_scheme.error, e, "\x1b[0m"
338 ));
339 } else {
340 result.push_str(&format!("arithmetic error: {}", e));
341 }
342 }
343 }
344 } else {
345 result.push_str("$((");
347 result.push_str(&arithmetic_expr);
348 }
350 continue;
351 }
352
353 let mut sub_command = String::new();
355 let mut paren_depth = 1;
356
357 for c in chars.by_ref() {
358 if c == '(' {
359 paren_depth += 1;
360 sub_command.push(c);
361 } else if c == ')' {
362 paren_depth -= 1;
363 if paren_depth == 0 {
364 break;
365 }
366 sub_command.push(c);
367 } else {
368 sub_command.push(c);
369 }
370 }
371
372 if let Ok(tokens) = crate::lexer::lex(&sub_command, shell_state) {
375 let expanded_tokens = match crate::lexer::expand_aliases(
377 tokens,
378 shell_state,
379 &mut std::collections::HashSet::new(),
380 ) {
381 Ok(t) => t,
382 Err(_) => {
383 result.push_str("$(");
385 result.push_str(&sub_command);
386 result.push(')');
387 continue;
388 }
389 };
390
391 match crate::parser::parse(expanded_tokens) {
392 Ok(ast) => {
393 match execute_and_capture_output(ast, shell_state) {
395 Ok(output) => {
396 result.push_str(&output);
397 }
398 Err(_) => {
399 result.push_str("$(");
401 result.push_str(&sub_command);
402 result.push(')');
403 }
404 }
405 }
406 Err(_parse_err) => {
407 let tokens_str = sub_command.trim();
409 if tokens_str.contains(' ') {
410 let parts: Vec<&str> = tokens_str.split_whitespace().collect();
412 if let Some(first_token) = parts.first()
413 && shell_state.get_function(first_token).is_some()
414 {
415 let function_call = Ast::FunctionCall {
417 name: first_token.to_string(),
418 args: parts[1..].iter().map(|s| s.to_string()).collect(),
419 };
420 match execute_and_capture_output(function_call, shell_state) {
421 Ok(output) => {
422 result.push_str(&output);
423 continue;
424 }
425 Err(_) => {
426 }
428 }
429 }
430 }
431 result.push_str("$(");
433 result.push_str(&sub_command);
434 result.push(')');
435 }
436 }
437 } else {
438 result.push_str("$(");
440 result.push_str(&sub_command);
441 result.push(')');
442 }
443 } else {
444 let mut var_name = String::new();
446 let mut next_ch = chars.peek();
447
448 if let Some(&c) = next_ch {
450 if c == '?' || c == '$' || c == '0' || c == '#' || c == '*' || c == '@' {
451 var_name.push(c);
452 chars.next(); } else if c.is_ascii_digit() {
454 var_name.push(c);
456 chars.next();
457 } else {
458 while let Some(&c) = next_ch {
460 if c.is_alphanumeric() || c == '_' {
461 var_name.push(c);
462 chars.next(); next_ch = chars.peek();
464 } else {
465 break;
466 }
467 }
468 }
469 }
470
471 if !var_name.is_empty() {
472 if let Some(value) = shell_state.get_var(&var_name) {
473 result.push_str(&value);
474 } else {
475 if var_name.chars().next().unwrap().is_ascii_digit()
478 || var_name == "?"
479 || var_name == "$"
480 || var_name == "0"
481 || var_name == "#"
482 || var_name == "*"
483 || var_name == "@"
484 {
485 } else {
487 result.push('$');
489 result.push_str(&var_name);
490 }
491 }
492 } else {
493 result.push('$');
494 }
495 }
496 } else if ch == '`' {
497 let mut sub_command = String::new();
499
500 for c in chars.by_ref() {
501 if c == '`' {
502 break;
503 }
504 sub_command.push(c);
505 }
506
507 if let Ok(tokens) = crate::lexer::lex(&sub_command, shell_state) {
509 let expanded_tokens = match crate::lexer::expand_aliases(
511 tokens,
512 shell_state,
513 &mut std::collections::HashSet::new(),
514 ) {
515 Ok(t) => t,
516 Err(_) => {
517 result.push('`');
519 result.push_str(&sub_command);
520 result.push('`');
521 continue;
522 }
523 };
524
525 if let Ok(ast) = crate::parser::parse(expanded_tokens) {
526 match execute_and_capture_output(ast, shell_state) {
528 Ok(output) => {
529 result.push_str(&output);
530 }
531 Err(_) => {
532 result.push('`');
534 result.push_str(&sub_command);
535 result.push('`');
536 }
537 }
538 } else {
539 result.push('`');
541 result.push_str(&sub_command);
542 result.push('`');
543 }
544 } else {
545 result.push('`');
547 result.push_str(&sub_command);
548 result.push('`');
549 }
550 } else {
551 result.push(ch);
552 }
553 }
554
555 result
556}
557
558fn expand_wildcards(args: &[String], shell_state: &ShellState) -> Result<Vec<String>, String> {
569 let mut expanded_args = Vec::new();
570
571 for arg in args {
572 if shell_state.options.noglob {
574 expanded_args.push(arg.clone());
575 continue;
576 }
577
578 if arg.contains('*') || arg.contains('?') || arg.contains('[') {
579 match glob::glob(arg) {
581 Ok(paths) => {
582 let mut matches: Vec<String> = paths
583 .filter_map(|p| p.ok())
584 .map(|p| p.to_string_lossy().to_string())
585 .collect();
586 if matches.is_empty() {
587 expanded_args.push(arg.clone());
589 } else {
590 matches.sort();
592 expanded_args.extend(matches);
593 }
594 }
595 Err(_e) => {
596 expanded_args.push(arg.clone());
598 }
599 }
600 } else {
601 expanded_args.push(arg.clone());
602 }
603 }
604 Ok(expanded_args)
605}
606
607fn write_file_with_noclobber(
612 path: &str,
613 data: &[u8],
614 noclobber: bool,
615 force_clobber: bool,
616 shell_state: &ShellState,
617) -> Result<(), String> {
618 use std::fs::OpenOptions;
619
620 if noclobber && !force_clobber {
621 let mut file = OpenOptions::new()
623 .write(true)
624 .create_new(true)
625 .open(path)
626 .map_err(|e| {
627 if e.kind() == std::io::ErrorKind::AlreadyExists {
628 if shell_state.colors_enabled {
629 format!(
630 "{}cannot overwrite existing file '{}' (noclobber is set)\x1b[0m",
631 shell_state.color_scheme.error, path
632 )
633 } else {
634 format!("cannot overwrite existing file '{}' (noclobber is set)", path)
635 }
636 } else {
637 if shell_state.colors_enabled {
638 format!(
639 "{}Cannot create {}: {}\x1b[0m",
640 shell_state.color_scheme.error, path, e
641 )
642 } else {
643 format!("Cannot create {}: {}", path, e)
644 }
645 }
646 })?;
647
648 file.write_all(data)
649 .map_err(|e| {
650 if shell_state.colors_enabled {
651 format!(
652 "{}Failed to write to {}: {}\x1b[0m",
653 shell_state.color_scheme.error, path, e
654 )
655 } else {
656 format!("Failed to write to {}: {}", path, e)
657 }
658 })?;
659 } else {
660 std::fs::write(path, data)
662 .map_err(|e| {
663 if shell_state.colors_enabled {
664 format!(
665 "{}Cannot write to {}: {}\x1b[0m",
666 shell_state.color_scheme.error, path, e
667 )
668 } else {
669 format!("Cannot write to {}: {}", path, e)
670 }
671 })?;
672 }
673
674 Ok(())
675}
676
677fn collect_here_document_content(delimiter: &str, shell_state: &mut ShellState) -> String {
681 if let Some(content) = shell_state.pending_heredoc_content.take() {
683 return content;
684 }
685
686 let stdin = std::io::stdin();
688 let mut reader = BufReader::new(stdin.lock());
689 let mut content = String::new();
690 let mut line = String::new();
691
692 loop {
693 line.clear();
694 match reader.read_line(&mut line) {
695 Ok(0) => {
696 break;
698 }
699 Ok(_) => {
700 let line_content = line.trim_end();
702 if line_content == delimiter {
703 break;
705 } else {
706 content.push_str(&line);
708 }
709 }
710 Err(e) => {
711 if shell_state.colors_enabled {
712 eprintln!(
713 "{}Error reading here-document content: {}\x1b[0m",
714 shell_state.color_scheme.error, e
715 );
716 } else {
717 eprintln!("Error reading here-document content: {}", e);
718 }
719 break;
720 }
721 }
722 }
723
724 content
725}
726
727fn apply_redirections(
750 redirections: &[Redirection],
751 shell_state: &mut ShellState,
752 mut command: Option<&mut Command>,
753) -> Result<(), String> {
754 for redir in redirections {
756 match redir {
757 Redirection::Input(file) => {
758 apply_input_redirection(0, file, shell_state, command.as_deref_mut())?;
759 }
760 Redirection::Output(file) => {
761 apply_output_redirection(1, file, false, false, shell_state, command.as_deref_mut())?;
762 }
763 Redirection::OutputClobber(file) => {
764 apply_output_redirection(1, file, false, true, shell_state, command.as_deref_mut())?;
765 }
766 Redirection::Append(file) => {
767 apply_output_redirection(1, file, true, false, shell_state, command.as_deref_mut())?;
768 }
769 Redirection::FdInput(fd, file) => {
770 apply_input_redirection(*fd, file, shell_state, command.as_deref_mut())?;
771 }
772 Redirection::FdOutput(fd, file) => {
773 apply_output_redirection(*fd, file, false, false, shell_state, command.as_deref_mut())?;
774 }
775 Redirection::FdOutputClobber(fd, file) => {
776 apply_output_redirection(*fd, file, false, true, shell_state, command.as_deref_mut())?;
777 }
778 Redirection::FdAppend(fd, file) => {
779 apply_output_redirection(*fd, file, true, false, shell_state, command.as_deref_mut())?;
780 }
781 Redirection::FdDuplicate(target_fd, source_fd) => {
782 apply_fd_duplication(*target_fd, *source_fd, shell_state, command.as_deref_mut())?;
783 }
784 Redirection::FdClose(fd) => {
785 apply_fd_close(*fd, shell_state, command.as_deref_mut())?;
786 }
787 Redirection::FdInputOutput(fd, file) => {
788 apply_fd_input_output(*fd, file, shell_state, command.as_deref_mut())?;
789 }
790 Redirection::HereDoc(delimiter, quoted_str) => {
791 let quoted = quoted_str == "true";
792 apply_heredoc_redirection(
793 0,
794 delimiter,
795 quoted,
796 shell_state,
797 command.as_deref_mut(),
798 )?;
799 }
800 Redirection::HereString(content) => {
801 apply_herestring_redirection(0, content, shell_state, command.as_deref_mut())?;
802 }
803 }
804 }
805 Ok(())
806}
807
808fn apply_input_redirection(
810 fd: i32,
811 file: &str,
812 shell_state: &mut ShellState,
813 command: Option<&mut Command>,
814) -> Result<(), String> {
815 let expanded_file = expand_variables_in_string(file, shell_state);
816
817 let file_handle =
819 File::open(&expanded_file).map_err(|e| format!("Cannot open {}: {}", expanded_file, e))?;
820
821 if fd == 0 {
822 if let Some(cmd) = command {
824 cmd.stdin(Stdio::from(file_handle));
825 } else {
826 shell_state.fd_table.borrow_mut().open_fd(
828 0,
829 &expanded_file,
830 true, false, false, false, false, )?;
836
837 let raw_fd = shell_state.fd_table.borrow().get_raw_fd(0);
839 if let Some(rfd) = raw_fd {
840 if rfd != 0 {
841 unsafe {
842 if libc::dup2(rfd, 0) < 0 {
843 return Err(format!("Failed to dup2 fd {} to 0", rfd));
844 }
845 }
846 }
847 }
848 }
849 } else {
850 let fd_file = File::open(&expanded_file)
853 .map_err(|e| format!("Cannot open {}: {}", expanded_file, e))?;
854
855 shell_state.fd_table.borrow_mut().open_fd(
857 fd,
858 &expanded_file,
859 true, false, false, false, false, )?;
865
866 if let Some(cmd) = command {
868 let target_fd = fd;
871 unsafe {
872 cmd.pre_exec(move || {
873 let raw_fd = fd_file.as_raw_fd();
874
875 if raw_fd != target_fd {
878 let result = libc::dup2(raw_fd, target_fd);
879 if result < 0 {
880 return Err(std::io::Error::last_os_error());
881 }
882 }
885 Ok(())
886 });
887 }
888 }
889 }
890
891 Ok(())
892}
893
894fn apply_output_redirection(
896 fd: i32,
897 file: &str,
898 append: bool,
899 force_clobber: bool,
900 shell_state: &mut ShellState,
901 command: Option<&mut Command>,
902) -> Result<(), String> {
903 let expanded_file = expand_variables_in_string(file, shell_state);
904
905 let file_handle = if append {
908 OpenOptions::new()
909 .append(true)
910 .create(true)
911 .open(&expanded_file)
912 .map_err(|e| {
913 if shell_state.colors_enabled {
914 format!("{}Cannot open {}: {}\x1b[0m", shell_state.color_scheme.error, expanded_file, e)
915 } else {
916 format!("Cannot open {}: {}", expanded_file, e)
917 }
918 })?
919 } else if shell_state.options.noclobber && !force_clobber {
920 OpenOptions::new()
922 .write(true)
923 .create_new(true)
924 .open(&expanded_file)
925 .map_err(|e| {
926 if e.kind() == std::io::ErrorKind::AlreadyExists {
927 if shell_state.colors_enabled {
928 format!(
929 "{}cannot overwrite existing file '{}' (noclobber is set)\x1b[0m",
930 shell_state.color_scheme.error, expanded_file
931 )
932 } else {
933 format!("cannot overwrite existing file '{}' (noclobber is set)", expanded_file)
934 }
935 } else {
936 if shell_state.colors_enabled {
937 format!("{}Cannot create {}: {}\x1b[0m", shell_state.color_scheme.error, expanded_file, e)
938 } else {
939 format!("Cannot create {}: {}", expanded_file, e)
940 }
941 }
942 })?
943 } else {
944 File::create(&expanded_file)
946 .map_err(|e| {
947 if shell_state.colors_enabled {
948 format!("{}Cannot create {}: {}\x1b[0m", shell_state.color_scheme.error, expanded_file, e)
949 } else {
950 format!("Cannot create {}: {}", expanded_file, e)
951 }
952 })?
953 };
954
955 if let Some(cmd) = command {
956 if fd == 1 {
957 cmd.stdout(Stdio::from(file_handle));
959 } else if fd == 2 {
960 cmd.stderr(Stdio::from(file_handle));
962 } else {
963 shell_state.fd_table.borrow_mut().open_fd(
968 fd,
969 &expanded_file,
970 false, true, append,
973 !append, false, )?;
976 }
977 } else {
978 if shell_state.options.noclobber && !force_clobber && !append {
981 if std::path::Path::new(&expanded_file).exists() {
983 let error_msg = if shell_state.colors_enabled {
984 format!(
985 "{}cannot overwrite existing file '{}' (noclobber is set)\x1b[0m",
986 shell_state.color_scheme.error, expanded_file
987 )
988 } else {
989 format!("cannot overwrite existing file '{}' (noclobber is set)", expanded_file)
990 };
991 return Err(error_msg);
992 }
993 }
994
995 shell_state.fd_table.borrow_mut().open_fd(
997 fd,
998 &expanded_file,
999 false, true, append,
1002 !append, shell_state.options.noclobber && !force_clobber && !append, )?;
1005
1006 let raw_fd = shell_state.fd_table.borrow().get_raw_fd(fd);
1009 if let Some(rfd) = raw_fd {
1010 if rfd != fd {
1012 unsafe {
1013 if libc::dup2(rfd, fd) < 0 {
1014 return Err(format!("Failed to dup2 fd {} to {}", rfd, fd));
1015 }
1016 }
1017 }
1018 }
1019 }
1020
1021 Ok(())
1022}
1023
1024fn apply_fd_duplication(
1026 target_fd: i32,
1027 source_fd: i32,
1028 shell_state: &mut ShellState,
1029 _command: Option<&mut Command>,
1030) -> Result<(), String> {
1031 if shell_state.fd_table.borrow().is_closed(source_fd) {
1033 let error_msg = format!("File descriptor {} is closed", source_fd);
1034 if shell_state.colors_enabled {
1035 eprintln!(
1036 "{}Redirection error: {}\x1b[0m",
1037 shell_state.color_scheme.error, error_msg
1038 );
1039 } else {
1040 eprintln!("Redirection error: {}", error_msg);
1041 }
1042 return Err(error_msg);
1043 }
1044
1045 shell_state
1047 .fd_table
1048 .borrow_mut()
1049 .duplicate_fd(source_fd, target_fd)?;
1050 Ok(())
1051}
1052
1053fn apply_fd_close(
1055 fd: i32,
1056 shell_state: &mut ShellState,
1057 command: Option<&mut Command>,
1058) -> Result<(), String> {
1059 shell_state.fd_table.borrow_mut().close_fd(fd)?;
1061
1062 if let Some(cmd) = command {
1065 match fd {
1066 0 => {
1067 cmd.stdin(Stdio::null());
1069 }
1070 1 => {
1071 cmd.stdout(Stdio::null());
1073 }
1074 2 => {
1075 cmd.stderr(Stdio::null());
1077 }
1078 _ => {
1079 }
1082 }
1083 }
1084
1085 Ok(())
1086}
1087
1088fn apply_fd_input_output(
1090 fd: i32,
1091 file: &str,
1092 shell_state: &mut ShellState,
1093 _command: Option<&mut Command>,
1094) -> Result<(), String> {
1095 let expanded_file = expand_variables_in_string(file, shell_state);
1096
1097 shell_state.fd_table.borrow_mut().open_fd(
1099 fd,
1100 &expanded_file,
1101 true, true, false, false, false, )?;
1107
1108 Ok(())
1109}
1110
1111fn apply_heredoc_redirection(
1113 fd: i32,
1114 delimiter: &str,
1115 quoted: bool,
1116 shell_state: &mut ShellState,
1117 command: Option<&mut Command>,
1118) -> Result<(), String> {
1119 let here_doc_content = collect_here_document_content(delimiter, shell_state);
1120
1121 let expanded_content = if quoted {
1123 here_doc_content
1124 } else {
1125 expand_variables_in_string(&here_doc_content, shell_state)
1126 };
1127
1128 let (reader, mut writer) =
1130 pipe().map_err(|e| format!("Failed to create pipe for here-document: {}", e))?;
1131
1132 writeln!(writer, "{}", expanded_content)
1133 .map_err(|e| format!("Failed to write here-document content: {}", e))?;
1134
1135 if fd == 0 {
1137 if let Some(cmd) = command {
1138 cmd.stdin(Stdio::from(reader));
1139 }
1140 }
1141
1142 Ok(())
1143}
1144
1145fn apply_herestring_redirection(
1147 fd: i32,
1148 content: &str,
1149 shell_state: &mut ShellState,
1150 command: Option<&mut Command>,
1151) -> Result<(), String> {
1152 let expanded_content = expand_variables_in_string(content, shell_state);
1153
1154 let (reader, mut writer) =
1156 pipe().map_err(|e| format!("Failed to create pipe for here-string: {}", e))?;
1157
1158 write!(writer, "{}", expanded_content)
1159 .map_err(|e| format!("Failed to write here-string content: {}", e))?;
1160
1161 if fd == 0 {
1163 if let Some(cmd) = command {
1164 cmd.stdin(Stdio::from(reader));
1165 }
1166 }
1167
1168 Ok(())
1169}
1170
1171pub fn execute_trap_handler(trap_cmd: &str, shell_state: &mut ShellState) -> i32 {
1174 let saved_exit_code = shell_state.last_exit_code;
1176
1177 let result = match crate::lexer::lex(trap_cmd, shell_state) {
1183 Ok(tokens) => {
1184 match crate::lexer::expand_aliases(
1185 tokens,
1186 shell_state,
1187 &mut std::collections::HashSet::new(),
1188 ) {
1189 Ok(expanded_tokens) => {
1190 match crate::parser::parse(expanded_tokens) {
1191 Ok(ast) => execute(ast, shell_state),
1192 Err(_) => {
1193 saved_exit_code
1195 }
1196 }
1197 }
1198 Err(_) => {
1199 saved_exit_code
1201 }
1202 }
1203 }
1204 Err(_) => {
1205 saved_exit_code
1207 }
1208 };
1209
1210 shell_state.last_exit_code = saved_exit_code;
1212
1213 result
1214}
1215
1216pub fn execute(ast: Ast, shell_state: &mut ShellState) -> i32 {
1238 match ast {
1239 Ast::Assignment { var, value } => {
1240 if shell_state.options.noexec {
1242 return 0; }
1244
1245 let expanded_value = expand_variables_in_string(&value, shell_state);
1247 shell_state.set_var(&var, expanded_value.clone());
1248
1249 if shell_state.options.allexport {
1251 shell_state.export_var(&var);
1252 }
1253 0
1254 }
1255 Ast::LocalAssignment { var, value } => {
1256 if shell_state.options.noexec {
1258 return 0; }
1260
1261 let expanded_value = expand_variables_in_string(&value, shell_state);
1263 shell_state.set_local_var(&var, expanded_value);
1264 0
1265 }
1266 Ast::Pipeline(commands) => {
1267 if commands.is_empty() {
1268 return 0;
1269 }
1270
1271 if commands.len() == 1 {
1272 execute_single_command(&commands[0], shell_state)
1274 } else {
1275 execute_pipeline(&commands, shell_state)
1277 }
1278 }
1279 Ast::Sequence(asts) => {
1280 let mut exit_code = 0;
1281 for ast in asts {
1282 shell_state.last_was_negation = false;
1284
1285 exit_code = execute(ast, shell_state);
1286
1287 if shell_state.is_returning() {
1289 return exit_code;
1290 }
1291
1292 if shell_state.exit_requested {
1294 return shell_state.exit_code;
1295 }
1296
1297 if shell_state.is_breaking() || shell_state.is_continuing() {
1299 return exit_code;
1300 }
1301
1302 if shell_state.options.errexit
1309 && exit_code != 0
1310 && !shell_state.in_condition
1311 && !shell_state.in_logical_chain
1312 && !shell_state.in_negation
1313 && !shell_state.last_was_negation {
1314 shell_state.exit_requested = true;
1316 shell_state.exit_code = exit_code;
1317 return exit_code;
1318 }
1319 }
1320 exit_code
1321 }
1322 Ast::If {
1323 branches,
1324 else_branch,
1325 } => {
1326 for (condition, then_branch) in branches {
1327 shell_state.in_condition = true;
1329 let cond_exit = execute(*condition, shell_state);
1330 shell_state.in_condition = false;
1331
1332 if cond_exit == 0 {
1333 let exit_code = execute(*then_branch, shell_state);
1334
1335 if shell_state.is_returning() {
1337 return exit_code;
1338 }
1339
1340 return exit_code;
1341 }
1342 }
1343 if let Some(else_b) = else_branch {
1344 let exit_code = execute(*else_b, shell_state);
1345
1346 if shell_state.is_returning() {
1348 return exit_code;
1349 }
1350
1351 exit_code
1352 } else {
1353 0
1354 }
1355 }
1356 Ast::Case {
1357 word,
1358 cases,
1359 default,
1360 } => {
1361 for (patterns, branch) in cases {
1362 for pattern in &patterns {
1363 if let Ok(glob_pattern) = glob::Pattern::new(pattern) {
1364 if glob_pattern.matches(&word) {
1365 let exit_code = execute(branch, shell_state);
1366
1367 if shell_state.is_returning() {
1369 return exit_code;
1370 }
1371
1372 return exit_code;
1373 }
1374 } else {
1375 if &word == pattern {
1377 let exit_code = execute(branch, shell_state);
1378
1379 if shell_state.is_returning() {
1381 return exit_code;
1382 }
1383
1384 return exit_code;
1385 }
1386 }
1387 }
1388 }
1389 if let Some(def) = default {
1390 let exit_code = execute(*def, shell_state);
1391
1392 if shell_state.is_returning() {
1394 return exit_code;
1395 }
1396
1397 exit_code
1398 } else {
1399 0
1400 }
1401 }
1402 Ast::For {
1403 variable,
1404 items,
1405 body,
1406 } => {
1407 let mut exit_code = 0;
1408
1409 shell_state.enter_loop();
1411
1412 let mut expanded_items = Vec::new();
1414 for item in items {
1415 let expanded = expand_variables_in_string(&item, shell_state);
1417
1418 for word in expanded.split_whitespace() {
1421 expanded_items.push(word.to_string());
1422 }
1423 }
1424
1425 for item in expanded_items {
1427 crate::state::process_pending_signals(shell_state);
1429
1430 if shell_state.exit_requested {
1432 shell_state.exit_loop();
1433 return shell_state.exit_code;
1434 }
1435
1436 shell_state.set_var(&variable, item.clone());
1438
1439 exit_code = execute(*body.clone(), shell_state);
1441
1442 if shell_state.is_returning() {
1444 shell_state.exit_loop();
1445 return exit_code;
1446 }
1447
1448 if shell_state.exit_requested {
1450 shell_state.exit_loop();
1451 return shell_state.exit_code;
1452 }
1453
1454 if shell_state.is_breaking() {
1456 if shell_state.get_break_level() == 1 {
1457 shell_state.clear_break();
1459 break;
1460 } else {
1461 shell_state.decrement_break_level();
1463 break;
1464 }
1465 }
1466
1467 if shell_state.is_continuing() {
1469 if shell_state.get_continue_level() == 1 {
1470 shell_state.clear_continue();
1472 continue;
1473 } else {
1474 shell_state.decrement_continue_level();
1476 break; }
1478 }
1479 }
1480
1481 shell_state.exit_loop();
1483
1484 exit_code
1485 }
1486 Ast::While { condition, body } => {
1487 let mut exit_code = 0;
1488
1489 shell_state.enter_loop();
1491
1492 loop {
1494 shell_state.in_condition = true;
1496 let cond_exit = execute(*condition.clone(), shell_state);
1497 shell_state.in_condition = false;
1498
1499 if shell_state.is_returning() {
1501 shell_state.exit_loop();
1502 return cond_exit;
1503 }
1504
1505 if shell_state.exit_requested {
1507 shell_state.exit_loop();
1508 return shell_state.exit_code;
1509 }
1510
1511 if cond_exit != 0 {
1513 break;
1514 }
1515
1516 exit_code = execute(*body.clone(), shell_state);
1518
1519 if shell_state.is_returning() {
1521 shell_state.exit_loop();
1522 return exit_code;
1523 }
1524
1525 if shell_state.exit_requested {
1527 shell_state.exit_loop();
1528 return shell_state.exit_code;
1529 }
1530
1531 if shell_state.is_breaking() {
1533 if shell_state.get_break_level() == 1 {
1534 shell_state.clear_break();
1536 break;
1537 } else {
1538 shell_state.decrement_break_level();
1540 break;
1541 }
1542 }
1543
1544 if shell_state.is_continuing() {
1546 if shell_state.get_continue_level() == 1 {
1547 shell_state.clear_continue();
1549 continue;
1550 } else {
1551 shell_state.decrement_continue_level();
1553 break; }
1555 }
1556 }
1557
1558 shell_state.exit_loop();
1560
1561 exit_code
1562 }
1563 Ast::Until { condition, body } => {
1564 let mut exit_code = 0;
1565
1566 shell_state.enter_loop();
1568
1569 loop {
1571 shell_state.in_condition = true;
1573 let cond_exit = execute(*condition.clone(), shell_state);
1574 shell_state.in_condition = false;
1575
1576 if shell_state.is_returning() {
1578 shell_state.exit_loop();
1579 return cond_exit;
1580 }
1581
1582 if shell_state.exit_requested {
1584 shell_state.exit_loop();
1585 return shell_state.exit_code;
1586 }
1587
1588 if cond_exit == 0 {
1590 break;
1591 }
1592
1593 exit_code = execute(*body.clone(), shell_state);
1595
1596 if shell_state.is_returning() {
1598 shell_state.exit_loop();
1599 return exit_code;
1600 }
1601
1602 if shell_state.exit_requested {
1604 shell_state.exit_loop();
1605 return shell_state.exit_code;
1606 }
1607
1608 if shell_state.is_breaking() {
1610 if shell_state.get_break_level() == 1 {
1611 shell_state.clear_break();
1613 break;
1614 } else {
1615 shell_state.decrement_break_level();
1617 break;
1618 }
1619 }
1620
1621 if shell_state.is_continuing() {
1623 if shell_state.get_continue_level() == 1 {
1624 shell_state.clear_continue();
1626 continue;
1627 } else {
1628 shell_state.decrement_continue_level();
1630 break; }
1632 }
1633 }
1634
1635 shell_state.exit_loop();
1637
1638 exit_code
1639 }
1640 Ast::FunctionDefinition { name, body } => {
1641 shell_state.define_function(name.clone(), *body);
1643 0
1644 }
1645 Ast::FunctionCall { name, args } => {
1646 if let Some(function_body) = shell_state.get_function(&name).cloned() {
1647 if shell_state.function_depth >= shell_state.max_recursion_depth {
1649 eprintln!(
1650 "Function recursion limit ({}) exceeded",
1651 shell_state.max_recursion_depth
1652 );
1653 return 1;
1654 }
1655
1656 shell_state.enter_function();
1658
1659 let old_positional = shell_state.positional_params.clone();
1661
1662 shell_state.set_positional_params(args.clone());
1664
1665 let exit_code = execute(function_body, shell_state);
1667
1668 if shell_state.is_returning() {
1670 let return_value = shell_state.get_return_value().unwrap_or(0);
1671
1672 shell_state.set_positional_params(old_positional);
1674
1675 shell_state.exit_function();
1677
1678 shell_state.clear_return();
1680
1681 shell_state.last_exit_code = return_value;
1683
1684 return return_value;
1686 }
1687
1688 shell_state.set_positional_params(old_positional);
1690
1691 shell_state.exit_function();
1693
1694 shell_state.last_exit_code = exit_code;
1696
1697 exit_code
1698 } else {
1699 eprintln!("Function '{}' not found", name);
1700 1
1701 }
1702 }
1703 Ast::Return { value } => {
1704 if shell_state.function_depth == 0 {
1706 eprintln!("Return statement outside of function");
1707 return 1;
1708 }
1709
1710 let exit_code = if let Some(ref val) = value {
1712 val.parse::<i32>().unwrap_or(0)
1713 } else {
1714 0
1715 };
1716
1717 shell_state.set_return(exit_code);
1719
1720 exit_code
1722 }
1723 Ast::And { left, right } => {
1724 shell_state.in_logical_chain = true;
1726
1727 let left_exit = execute(*left, shell_state);
1729
1730 if shell_state.is_returning()
1733 || shell_state.exit_requested
1734 || shell_state.is_breaking()
1735 || shell_state.is_continuing()
1736 {
1737 shell_state.in_logical_chain = false;
1738 return left_exit;
1739 }
1740
1741 let result = if left_exit == 0 {
1743 execute(*right, shell_state)
1744 } else {
1745 left_exit
1746 };
1747
1748 shell_state.in_logical_chain = false;
1749 result
1750 }
1751 Ast::Or { left, right } => {
1752 shell_state.in_logical_chain = true;
1754
1755 let left_exit = execute(*left, shell_state);
1757
1758 if shell_state.is_returning()
1761 || shell_state.exit_requested
1762 || shell_state.is_breaking()
1763 || shell_state.is_continuing()
1764 {
1765 shell_state.in_logical_chain = false;
1766 return left_exit;
1767 }
1768
1769 let result = if left_exit != 0 {
1771 execute(*right, shell_state)
1772 } else {
1773 left_exit
1774 };
1775
1776 shell_state.in_logical_chain = false;
1777 result
1778 }
1779 Ast::Negation { command } => {
1780 shell_state.in_negation = true;
1782
1783 let exit_code = execute(*command, shell_state);
1785
1786 shell_state.in_negation = false;
1788
1789 shell_state.last_was_negation = true;
1791
1792 let inverted_code = if exit_code == 0 { 1 } else { 0 };
1794
1795 shell_state.last_exit_code = inverted_code;
1797
1798 inverted_code
1799 }
1800 Ast::Subshell { body } => {
1801 let exit_code = execute_subshell(*body, shell_state);
1802
1803 if shell_state.options.errexit
1809 && exit_code != 0
1810 && !shell_state.in_condition
1811 && !shell_state.in_logical_chain
1812 && !shell_state.in_negation {
1813 shell_state.exit_requested = true;
1815 shell_state.exit_code = exit_code;
1816 }
1817
1818 exit_code
1819 }
1820 Ast::CommandGroup { body } => execute(*body, shell_state),
1821 }
1822}
1823
1824fn execute_single_command(cmd: &ShellCommand, shell_state: &mut ShellState) -> i32 {
1840 if let Some(ref compound_ast) = cmd.compound {
1842 if shell_state.options.noexec {
1845 return 0; }
1847 return execute_compound_with_redirections(compound_ast, shell_state, &cmd.redirections);
1849 }
1850
1851 let is_set_builtin = !cmd.args.is_empty() && cmd.args[0] == "set";
1855
1856 if shell_state.options.noexec && !is_set_builtin {
1857 return 0; }
1859
1860 if cmd.args.is_empty() {
1861 if !cmd.redirections.is_empty() {
1863 if let Err(e) = apply_redirections(&cmd.redirections, shell_state, None) {
1864 if shell_state.colors_enabled {
1865 eprintln!(
1866 "{}Redirection error: {}\x1b[0m",
1867 shell_state.color_scheme.error, e
1868 );
1869 } else {
1870 eprintln!("Redirection error: {}", e);
1871 }
1872 return 1;
1873 }
1874 }
1875 return 0;
1876 }
1877
1878 let var_expanded_args = expand_variables_in_args(&cmd.args, shell_state);
1880 let expanded_args = match expand_wildcards(&var_expanded_args, shell_state) {
1881 Ok(args) => args,
1882 Err(_) => return 1,
1883 };
1884
1885 if expanded_args.is_empty() {
1886 return 0;
1887 }
1888
1889 if shell_state.options.xtrace {
1891 let ps4 = shell_state.get_var("PS4").unwrap_or_else(|| "+ ".to_string());
1893
1894 let command_str = expanded_args.join(" ");
1896 if shell_state.colors_enabled {
1897 eprintln!(
1898 "{}{}{}\x1b[0m",
1899 shell_state.color_scheme.builtin,
1900 ps4,
1901 command_str
1902 );
1903 } else {
1904 eprintln!("{}{}", ps4, command_str);
1905 }
1906 }
1907
1908 if shell_state.get_function(&expanded_args[0]).is_some() {
1910 let function_call = Ast::FunctionCall {
1912 name: expanded_args[0].clone(),
1913 args: expanded_args[1..].to_vec(),
1914 };
1915 return execute(function_call, shell_state);
1916 }
1917
1918 if crate::builtins::is_builtin(&expanded_args[0]) {
1919 let temp_cmd = ShellCommand {
1921 args: expanded_args,
1922 redirections: cmd.redirections.clone(),
1923 compound: None,
1924 };
1925
1926 let exit_code = if let Some(ref capture_buffer) = shell_state.capture_output.clone() {
1928 struct CaptureWriter {
1930 buffer: Rc<RefCell<Vec<u8>>>,
1931 }
1932 impl std::io::Write for CaptureWriter {
1933 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1934 self.buffer.borrow_mut().extend_from_slice(buf);
1935 Ok(buf.len())
1936 }
1937 fn flush(&mut self) -> std::io::Result<()> {
1938 Ok(())
1939 }
1940 }
1941 let writer = CaptureWriter {
1942 buffer: capture_buffer.clone(),
1943 };
1944 crate::builtins::execute_builtin(&temp_cmd, shell_state, Some(Box::new(writer)))
1945 } else {
1946 crate::builtins::execute_builtin(&temp_cmd, shell_state, None)
1947 };
1948
1949 if shell_state.options.errexit
1956 && exit_code != 0
1957 && !shell_state.in_condition
1958 && !shell_state.in_logical_chain
1959 && !shell_state.in_negation {
1960 shell_state.exit_requested = true;
1962 shell_state.exit_code = exit_code;
1963 }
1964
1965 exit_code
1966 } else {
1967 let mut env_assignments = Vec::new();
1970 let mut command_start_idx = 0;
1971
1972 for (idx, arg) in expanded_args.iter().enumerate() {
1973 if let Some(eq_pos) = arg.find('=')
1975 && eq_pos > 0
1976 {
1977 let var_part = &arg[..eq_pos];
1978 if var_part
1980 .chars()
1981 .next()
1982 .map(|c| c.is_alphabetic() || c == '_')
1983 .unwrap_or(false)
1984 && var_part.chars().all(|c| c.is_alphanumeric() || c == '_')
1985 {
1986 env_assignments.push(arg.clone());
1987 command_start_idx = idx + 1;
1988 continue;
1989 }
1990 }
1991 break;
1993 }
1994
1995 let has_command = command_start_idx < expanded_args.len();
1997
1998 if !has_command {
2001 for assignment in &env_assignments {
2002 if let Some(eq_pos) = assignment.find('=') {
2003 let var_name = &assignment[..eq_pos];
2004 let var_value = &assignment[eq_pos + 1..];
2005 shell_state.set_var(var_name, var_value.to_string());
2006
2007 if shell_state.options.allexport {
2009 shell_state.export_var(var_name);
2010 }
2011 }
2012 }
2013
2014 if !cmd.redirections.is_empty() {
2016 if let Err(e) = apply_redirections(&cmd.redirections, shell_state, None) {
2017 if shell_state.colors_enabled {
2018 eprintln!(
2019 "{}Redirection error: {}\x1b[0m",
2020 shell_state.color_scheme.error, e
2021 );
2022 } else {
2023 eprintln!("Redirection error: {}", e);
2024 }
2025 return 1;
2026 }
2027 }
2028 return 0;
2029 }
2030
2031 let mut command = Command::new(&expanded_args[command_start_idx]);
2033 command.args(&expanded_args[command_start_idx + 1..]);
2034
2035 if let Some(fd) = shell_state.stdin_override {
2037 unsafe {
2038 let dup_fd = libc::dup(fd);
2039 if dup_fd >= 0 {
2040 command.stdin(Stdio::from_raw_fd(dup_fd));
2041 }
2042 }
2043 }
2044
2045 let mut child_env = shell_state.get_env_for_child();
2047
2048 for assignment in env_assignments {
2050 if let Some(eq_pos) = assignment.find('=') {
2051 let var_name = assignment[..eq_pos].to_string();
2052 let var_value = assignment[eq_pos + 1..].to_string();
2053 child_env.insert(var_name, var_value);
2054 }
2055 }
2056
2057 command.env_clear();
2058 for (key, value) in child_env {
2059 command.env(key, value);
2060 }
2061
2062 let capturing = shell_state.capture_output.is_some();
2064 if capturing {
2065 command.stdout(Stdio::piped());
2066 }
2067
2068 if let Err(e) = apply_redirections(&cmd.redirections, shell_state, Some(&mut command)) {
2070 if shell_state.colors_enabled {
2071 eprintln!(
2072 "{}Redirection error: {}\x1b[0m",
2073 shell_state.color_scheme.error, e
2074 );
2075 } else {
2076 eprintln!("Redirection error: {}", e);
2077 }
2078 return 1;
2079 }
2080
2081 let custom_fds: Vec<(i32, RawFd)> = {
2085 let fd_table = shell_state.fd_table.borrow();
2086 let mut fds = Vec::new();
2087
2088 for fd_num in 3..=9 {
2089 if fd_table.is_open(fd_num) {
2090 if let Some(raw_fd) = fd_table.get_raw_fd(fd_num) {
2091 fds.push((fd_num, raw_fd));
2092 }
2093 }
2094 }
2095
2096 fds
2097 };
2098
2099 if !custom_fds.is_empty() {
2101 unsafe {
2102 command.pre_exec(move || {
2103 for (target_fd, source_fd) in &custom_fds {
2104 let result = libc::dup2(*source_fd, *target_fd);
2105 if result < 0 {
2106 return Err(std::io::Error::last_os_error());
2107 }
2108 }
2109 Ok(())
2110 });
2111 }
2112 }
2113
2114 match command.spawn() {
2118 Ok(mut child) => {
2119 if capturing {
2121 if let Some(mut stdout) = child.stdout.take() {
2122 use std::io::Read;
2123 let mut output = Vec::new();
2124 if stdout.read_to_end(&mut output).is_ok() {
2125 if let Some(ref capture_buffer) = shell_state.capture_output {
2126 capture_buffer.borrow_mut().extend_from_slice(&output);
2127 }
2128 }
2129 }
2130 }
2131
2132 let exit_code = match child.wait() {
2133 Ok(status) => status.code().unwrap_or(0),
2134 Err(e) => {
2135 if shell_state.colors_enabled {
2136 eprintln!(
2137 "{}Error waiting for command: {}\x1b[0m",
2138 shell_state.color_scheme.error, e
2139 );
2140 } else {
2141 eprintln!("Error waiting for command: {}", e);
2142 }
2143 1
2144 }
2145 };
2146
2147 if shell_state.options.errexit
2154 && exit_code != 0
2155 && !shell_state.in_condition
2156 && !shell_state.in_logical_chain
2157 && !shell_state.in_negation {
2158 shell_state.exit_requested = true;
2160 shell_state.exit_code = exit_code;
2161 }
2162
2163 exit_code
2164 }
2165 Err(e) => {
2166 if shell_state.colors_enabled {
2167 eprintln!(
2168 "{}Command spawn error: {}\x1b[0m",
2169 shell_state.color_scheme.error, e
2170 );
2171 } else {
2172 eprintln!("Command spawn error: {}", e);
2173 }
2174 1
2175 }
2176 }
2177 }
2178}
2179
2180fn execute_pipeline(commands: &[ShellCommand], shell_state: &mut ShellState) -> i32 {
2197 let has_set_builtin = commands.iter().any(|cmd| {
2201 !cmd.args.is_empty() && cmd.args[0] == "set"
2202 });
2203
2204 if shell_state.options.noexec && !has_set_builtin {
2205 return 0; }
2207
2208 let mut exit_code = 0;
2209 let mut previous_stdout: Option<File> = None;
2210
2211 for (i, cmd) in commands.iter().enumerate() {
2212 let is_last = i == commands.len() - 1;
2213
2214 if let Some(ref compound_ast) = cmd.compound {
2215 let (com_exit_code, com_stdout) = execute_compound_in_pipeline(
2217 compound_ast,
2218 shell_state,
2219 previous_stdout.take(),
2220 i == 0,
2221 is_last,
2222 &cmd.redirections,
2223 );
2224 exit_code = com_exit_code;
2225 previous_stdout = com_stdout;
2226 continue;
2227 }
2228
2229 if cmd.args.is_empty() {
2230 continue;
2231 }
2232
2233 let var_expanded_args = expand_variables_in_args(&cmd.args, shell_state);
2235 let expanded_args = match expand_wildcards(&var_expanded_args, shell_state) {
2236 Ok(args) => args,
2237 Err(_) => return 1,
2238 };
2239
2240 if expanded_args.is_empty() {
2241 continue;
2242 }
2243
2244 if crate::builtins::is_builtin(&expanded_args[0]) {
2245 let temp_cmd = ShellCommand {
2248 args: expanded_args,
2249 redirections: cmd.redirections.clone(),
2250 compound: None,
2251 };
2252 if !is_last {
2253 let (reader, writer) = match pipe() {
2255 Ok((r, w)) => (unsafe { File::from_raw_fd(r.into_raw_fd()) }, w),
2256 Err(e) => {
2257 if shell_state.colors_enabled {
2258 eprintln!(
2259 "{}Error creating pipe for builtin: {}\x1b[0m",
2260 shell_state.color_scheme.error, e
2261 );
2262 } else {
2263 eprintln!("Error creating pipe for builtin: {}", e);
2264 }
2265 return 1;
2266 }
2267 };
2268 exit_code = crate::builtins::execute_builtin(
2270 &temp_cmd,
2271 shell_state,
2272 Some(Box::new(writer)),
2273 );
2274 previous_stdout = Some(reader);
2276 } else {
2277 if let Some(ref capture_buffer) = shell_state.capture_output.clone() {
2279 struct CaptureWriter {
2281 buffer: Rc<RefCell<Vec<u8>>>,
2282 }
2283 impl std::io::Write for CaptureWriter {
2284 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
2285 self.buffer.borrow_mut().extend_from_slice(buf);
2286 Ok(buf.len())
2287 }
2288 fn flush(&mut self) -> std::io::Result<()> {
2289 Ok(())
2290 }
2291 }
2292 let writer = CaptureWriter {
2293 buffer: capture_buffer.clone(),
2294 };
2295 exit_code = crate::builtins::execute_builtin(
2296 &temp_cmd,
2297 shell_state,
2298 Some(Box::new(writer)),
2299 );
2300 } else {
2301 exit_code = crate::builtins::execute_builtin(&temp_cmd, shell_state, None);
2303 }
2304 previous_stdout = None;
2305 }
2306 } else {
2307 let mut command = Command::new(&expanded_args[0]);
2308 command.args(&expanded_args[1..]);
2309
2310 let child_env = shell_state.get_env_for_child();
2312 command.env_clear();
2313 for (key, value) in child_env {
2314 command.env(key, value);
2315 }
2316
2317 if let Some(prev) = previous_stdout.take() {
2319 command.stdin(Stdio::from(prev));
2320 } else if i > 0 {
2321 command.stdin(Stdio::null());
2325 } else if let Some(fd) = shell_state.stdin_override {
2326 unsafe {
2329 let dup_fd = libc::dup(fd);
2330 if dup_fd >= 0 {
2331 command.stdin(Stdio::from_raw_fd(dup_fd));
2332 }
2333 }
2334 }
2335
2336 if !is_last {
2338 command.stdout(Stdio::piped());
2339 } else if shell_state.capture_output.is_some() {
2340 command.stdout(Stdio::piped());
2342 }
2343
2344 if let Err(e) = apply_redirections(&cmd.redirections, shell_state, Some(&mut command)) {
2346 if shell_state.colors_enabled {
2347 eprintln!(
2348 "{}Redirection error: {}\x1b[0m",
2349 shell_state.color_scheme.error, e
2350 );
2351 } else {
2352 eprintln!("Redirection error: {}", e);
2353 }
2354 return 1;
2355 }
2356
2357 match command.spawn() {
2358 Ok(mut child) => {
2359 if !is_last {
2360 previous_stdout = child
2361 .stdout
2362 .take()
2363 .map(|s| unsafe { File::from_raw_fd(s.into_raw_fd()) });
2364 } else if shell_state.capture_output.is_some() {
2365 if let Some(mut stdout) = child.stdout.take() {
2367 use std::io::Read;
2368 let mut output = Vec::new();
2369 if stdout.read_to_end(&mut output).is_ok()
2370 && let Some(ref capture_buffer) = shell_state.capture_output
2371 {
2372 capture_buffer.borrow_mut().extend_from_slice(&output);
2373 }
2374 }
2375 }
2376 match child.wait() {
2377 Ok(status) => {
2378 exit_code = status.code().unwrap_or(0);
2379 }
2380 Err(e) => {
2381 if shell_state.colors_enabled {
2382 eprintln!(
2383 "{}Error waiting for command: {}\x1b[0m",
2384 shell_state.color_scheme.error, e
2385 );
2386 } else {
2387 eprintln!("Error waiting for command: {}", e);
2388 }
2389 exit_code = 1;
2390 }
2391 }
2392 }
2393 Err(e) => {
2394 if shell_state.colors_enabled {
2395 eprintln!(
2396 "{}Error spawning command '{}{}",
2397 shell_state.color_scheme.error,
2398 expanded_args[0],
2399 &format!("': {}\x1b[0m", e)
2400 );
2401 } else {
2402 eprintln!("Error spawning command '{}': {}", expanded_args[0], e);
2403 }
2404 exit_code = 1;
2405 }
2406 }
2407 }
2408 }
2409
2410 exit_code
2411}
2412
2413fn execute_subshell(body: Ast, shell_state: &mut ShellState) -> i32 {
2431 if shell_state.subshell_depth >= MAX_SUBSHELL_DEPTH {
2433 if shell_state.colors_enabled {
2434 eprintln!(
2435 "{}Subshell nesting limit ({}) exceeded\x1b[0m",
2436 shell_state.color_scheme.error, MAX_SUBSHELL_DEPTH
2437 );
2438 } else {
2439 eprintln!("Subshell nesting limit ({}) exceeded", MAX_SUBSHELL_DEPTH);
2440 }
2441 shell_state.last_exit_code = 1;
2442 return 1;
2443 }
2444
2445 let original_dir = std::env::current_dir().ok();
2447
2448 let mut subshell_state = shell_state.clone();
2450
2451 match shell_state.fd_table.borrow().deep_clone() {
2455 Ok(new_fd_table) => {
2456 subshell_state.fd_table = Rc::new(RefCell::new(new_fd_table));
2457 }
2458 Err(e) => {
2459 if shell_state.colors_enabled {
2460 eprintln!(
2461 "{}Failed to clone file descriptor table: {}\x1b[0m",
2462 shell_state.color_scheme.error, e
2463 );
2464 } else {
2465 eprintln!("Failed to clone file descriptor table: {}", e);
2466 }
2467 return 1;
2468 }
2469 }
2470
2471 subshell_state.subshell_depth = shell_state.subshell_depth + 1;
2473
2474 let parent_traps = shell_state.trap_handlers.lock().unwrap().clone();
2476 subshell_state.trap_handlers = std::sync::Arc::new(std::sync::Mutex::new(parent_traps));
2477
2478 let exit_code = execute(body, &mut subshell_state);
2480
2481 let final_exit_code = if subshell_state.exit_requested {
2484 subshell_state.exit_code
2486 } else if subshell_state.is_returning() {
2487 subshell_state.get_return_value().unwrap_or(exit_code)
2490 } else {
2491 exit_code
2492 };
2493
2494 subshell_state.fd_table.borrow_mut().clear();
2497
2498 if let Some(dir) = original_dir {
2500 let _ = std::env::set_current_dir(dir);
2501 }
2502
2503 shell_state.last_exit_code = final_exit_code;
2505
2506 final_exit_code
2508}
2509
2510fn execute_compound_with_redirections(
2520 compound_ast: &Ast,
2521 shell_state: &mut ShellState,
2522 redirections: &[Redirection],
2523) -> i32 {
2524 match compound_ast {
2525 Ast::CommandGroup { body } => {
2526 if let Err(e) = shell_state.fd_table.borrow_mut().save_all_fds() {
2528 eprintln!("Error saving FDs: {}", e);
2529 return 1;
2530 }
2531
2532 if let Err(e) = apply_redirections(redirections, shell_state, None) {
2534 if shell_state.colors_enabled {
2535 eprintln!("{}{}\u{001b}[0m", shell_state.color_scheme.error, e);
2536 } else {
2537 eprintln!("{}", e);
2538 }
2539 shell_state.fd_table.borrow_mut().restore_all_fds().ok();
2540 return 1;
2541 }
2542
2543 let exit_code = execute(*body.clone(), shell_state);
2545
2546 if let Err(e) = shell_state.fd_table.borrow_mut().restore_all_fds() {
2548 eprintln!("Error restoring FDs: {}", e);
2549 }
2550
2551 exit_code
2552 }
2553 Ast::Subshell { body } => {
2554 let has_output_redir = redirections.iter().any(|r| {
2561 matches!(
2562 r,
2563 Redirection::Output(_)
2564 | Redirection::Append(_)
2565 | Redirection::FdOutput(_, _)
2566 | Redirection::FdAppend(_, _)
2567 )
2568 });
2569
2570 if has_output_redir {
2571 let mut subshell_state = shell_state.clone();
2573
2574 let capture_buffer = Rc::new(RefCell::new(Vec::new()));
2576 subshell_state.capture_output = Some(capture_buffer.clone());
2577
2578 let exit_code = execute(*body.clone(), &mut subshell_state);
2580
2581 let output = capture_buffer.borrow().clone();
2583
2584 for redir in redirections {
2586 match redir {
2587 Redirection::Output(file) => {
2588 let expanded_file = expand_variables_in_string(file, shell_state);
2589
2590 if let Err(e) = write_file_with_noclobber(
2592 &expanded_file,
2593 &output,
2594 shell_state.options.noclobber,
2595 false, shell_state,
2597 ) {
2598 eprintln!("Redirection error: {}", e);
2599 return 1;
2600 }
2601 }
2602 Redirection::OutputClobber(file) => {
2603 let expanded_file = expand_variables_in_string(file, shell_state);
2604 if let Err(e) = write_file_with_noclobber(
2606 &expanded_file,
2607 &output,
2608 false, true, shell_state,
2611 ) {
2612 eprintln!("Redirection error: {}", e);
2613 return 1;
2614 }
2615 }
2616 Redirection::Append(file) => {
2617 let expanded_file = expand_variables_in_string(file, shell_state);
2618 use std::fs::OpenOptions;
2619 let mut file_handle = match OpenOptions::new()
2620 .append(true)
2621 .create(true)
2622 .open(&expanded_file)
2623 {
2624 Ok(f) => f,
2625 Err(e) => {
2626 if shell_state.colors_enabled {
2627 eprintln!(
2628 "{}Redirection error: {}\x1b[0m",
2629 shell_state.color_scheme.error, e
2630 );
2631 } else {
2632 eprintln!("Redirection error: {}", e);
2633 }
2634 return 1;
2635 }
2636 };
2637 if let Err(e) = file_handle.write_all(&output) {
2638 if shell_state.colors_enabled {
2639 eprintln!(
2640 "{}Redirection error: {}\x1b[0m",
2641 shell_state.color_scheme.error, e
2642 );
2643 } else {
2644 eprintln!("Redirection error: {}", e);
2645 }
2646 return 1;
2647 }
2648 }
2649 _ => {
2650 }
2653 }
2654 }
2655
2656 shell_state.last_exit_code = exit_code;
2657 exit_code
2658 } else {
2659 execute_subshell(*body.clone(), shell_state)
2661 }
2662 }
2663 _ => {
2664 eprintln!("Unsupported compound command type");
2665 1
2666 }
2667 }
2668}
2669
2670fn has_stdout_redirection(redirections: &[Redirection]) -> bool {
2673 redirections.iter().any(|r| match r {
2674 Redirection::Output(_) | Redirection::OutputClobber(_) | Redirection::Append(_) => true,
2676 Redirection::FdOutput(1, _) | Redirection::FdAppend(1, _) => true,
2678 Redirection::FdDuplicate(1, _) | Redirection::FdClose(1) => true,
2680 _ => false,
2682 })
2683}
2684
2685fn execute_compound_in_pipeline(
2696 compound_ast: &Ast,
2697 shell_state: &mut ShellState,
2698 stdin: Option<File>,
2699 is_first: bool,
2700 is_last: bool,
2701 redirections: &[Redirection],
2702) -> (i32, Option<File>) {
2703 match compound_ast {
2704 Ast::Subshell { body } | Ast::CommandGroup { body } => {
2705 let mut subshell_state = shell_state.clone();
2707
2708 let mut _stdin_file = stdin;
2711
2712 if let Some(ref f) = _stdin_file {
2713 let fd = f.as_raw_fd();
2714 subshell_state.stdin_override = Some(fd);
2715 } else if !is_first && subshell_state.stdin_override.is_none() {
2716 if let Ok(f) = File::open("/dev/null") {
2718 subshell_state.stdin_override = Some(f.as_raw_fd());
2719 _stdin_file = Some(f);
2720 }
2721 }
2722
2723 let capture_buffer = if (!is_last || shell_state.capture_output.is_some())
2726 && !has_stdout_redirection(redirections)
2727 {
2728 let buffer = Rc::new(RefCell::new(Vec::new()));
2729 subshell_state.capture_output = Some(buffer.clone());
2730 Some(buffer)
2731 } else {
2732 None
2733 };
2734
2735 let exit_code = if matches!(compound_ast, Ast::CommandGroup { .. }) {
2737 if let Err(e) = subshell_state.fd_table.borrow_mut().save_all_fds() {
2739 eprintln!("Error saving FDs: {}", e);
2740 return (1, None);
2741 }
2742
2743 if let Some(ref f) = _stdin_file {
2745 unsafe {
2746 libc::dup2(f.as_raw_fd(), 0);
2747 }
2748 }
2749
2750 if let Err(e) = apply_redirections(redirections, &mut subshell_state, None) {
2752 if subshell_state.colors_enabled {
2753 eprintln!("{}{}\u{001b}[0m", subshell_state.color_scheme.error, e);
2754 } else {
2755 eprintln!("{}", e);
2756 }
2757 subshell_state.fd_table.borrow_mut().restore_all_fds().ok();
2758 return (1, None);
2759 }
2760
2761 let code = execute(*body.clone(), &mut subshell_state);
2763
2764 if let Err(e) = subshell_state.fd_table.borrow_mut().restore_all_fds() {
2766 eprintln!("Error restoring FDs: {}", e);
2767 }
2768 code
2769 } else {
2770 if let Err(e) = subshell_state.fd_table.borrow_mut().save_all_fds() {
2772 eprintln!("Error saving FDs: {}", e);
2773 return (1, None);
2774 }
2775
2776 if let Some(ref f) = _stdin_file {
2778 unsafe {
2779 libc::dup2(f.as_raw_fd(), 0);
2780 }
2781 }
2782
2783 if let Err(e) = apply_redirections(redirections, &mut subshell_state, None) {
2784 eprintln!("{}", e);
2785 subshell_state.fd_table.borrow_mut().restore_all_fds().ok();
2786 return (1, None);
2787 }
2788 let code = execute(*body.clone(), &mut subshell_state);
2789 subshell_state.fd_table.borrow_mut().restore_all_fds().ok();
2790 code
2791 };
2792
2793 let mut next_stdout = None;
2795 if let Some(buffer) = capture_buffer {
2796 let captured = buffer.borrow().clone();
2797
2798 if !is_last {
2800 use std::io::Write;
2801 let (reader, mut writer) = match pipe() {
2802 Ok((r, w)) => (r, w),
2803 Err(e) => {
2804 eprintln!("Error creating pipe for compound command: {}", e);
2805 return (exit_code, None);
2806 }
2807 };
2808 if let Err(e) = writer.write_all(&captured) {
2809 eprintln!("Error writing to pipe: {}", e);
2810 }
2811 drop(writer); next_stdout = Some(unsafe { File::from_raw_fd(reader.into_raw_fd()) });
2814 }
2815
2816 if let Some(ref parent_capture) = shell_state.capture_output {
2818 parent_capture.borrow_mut().extend_from_slice(&captured);
2819 }
2820 }
2821
2822 shell_state.last_exit_code = exit_code;
2823 (exit_code, next_stdout)
2824 }
2825 _ => {
2826 eprintln!("Unsupported compound command in pipeline");
2827 (1, None)
2828 }
2829 }
2830}
2831
2832#[cfg(test)]
2833mod tests {
2834 use super::*;
2835 use std::sync::Mutex;
2836
2837 static ENV_LOCK: Mutex<()> = Mutex::new(());
2839
2840 #[test]
2841 fn test_execute_single_command_builtin() {
2842 let cmd = ShellCommand {
2843 args: vec!["true".to_string()],
2844 redirections: Vec::new(),
2845 compound: None,
2846 };
2847 let mut shell_state = ShellState::new();
2848 let exit_code = execute_single_command(&cmd, &mut shell_state);
2849 assert_eq!(exit_code, 0);
2850 }
2851
2852 #[test]
2854 fn test_execute_single_command_external() {
2855 let cmd = ShellCommand {
2856 args: vec!["true".to_string()], redirections: Vec::new(),
2858 compound: None,
2859 };
2860 let mut shell_state = ShellState::new();
2861 let exit_code = execute_single_command(&cmd, &mut shell_state);
2862 assert_eq!(exit_code, 0);
2863 }
2864
2865 #[test]
2866 fn test_execute_single_command_external_nonexistent() {
2867 let cmd = ShellCommand {
2868 args: vec!["nonexistent_command".to_string()],
2869 redirections: Vec::new(),
2870 compound: None,
2871 };
2872 let mut shell_state = ShellState::new();
2873 let exit_code = execute_single_command(&cmd, &mut shell_state);
2874 assert_eq!(exit_code, 1); }
2876
2877 #[test]
2878 fn test_execute_pipeline() {
2879 let commands = vec![
2880 ShellCommand {
2881 args: vec!["printf".to_string(), "hello".to_string()],
2882 redirections: Vec::new(),
2883 compound: None,
2884 },
2885 ShellCommand {
2886 args: vec!["cat".to_string()], redirections: Vec::new(),
2888 compound: None,
2889 },
2890 ];
2891 let mut shell_state = ShellState::new();
2892 let exit_code = execute_pipeline(&commands, &mut shell_state);
2893 assert_eq!(exit_code, 0);
2894 }
2895
2896 #[test]
2897 fn test_execute_empty_pipeline() {
2898 let commands = vec![];
2899 let mut shell_state = ShellState::new();
2900 let exit_code = execute(Ast::Pipeline(commands), &mut shell_state);
2901 assert_eq!(exit_code, 0);
2902 }
2903
2904 #[test]
2905 fn test_execute_single_command() {
2906 let ast = Ast::Pipeline(vec![ShellCommand {
2907 args: vec!["true".to_string()],
2908 redirections: Vec::new(),
2909 compound: None,
2910 }]);
2911 let mut shell_state = ShellState::new();
2912 let exit_code = execute(ast, &mut shell_state);
2913 assert_eq!(exit_code, 0);
2914 }
2915
2916 #[test]
2917 fn test_execute_function_definition() {
2918 let ast = Ast::FunctionDefinition {
2919 name: "test_func".to_string(),
2920 body: Box::new(Ast::Pipeline(vec![ShellCommand {
2921 args: vec!["echo".to_string(), "hello".to_string()],
2922 redirections: Vec::new(),
2923 compound: None,
2924 }])),
2925 };
2926 let mut shell_state = ShellState::new();
2927 let exit_code = execute(ast, &mut shell_state);
2928 assert_eq!(exit_code, 0);
2929
2930 assert!(shell_state.get_function("test_func").is_some());
2932 }
2933
2934 #[test]
2935 fn test_execute_function_call() {
2936 let mut shell_state = ShellState::new();
2938 shell_state.define_function(
2939 "test_func".to_string(),
2940 Ast::Pipeline(vec![ShellCommand {
2941 args: vec!["echo".to_string(), "hello".to_string()],
2942 redirections: Vec::new(),
2943 compound: None,
2944 }]),
2945 );
2946
2947 let ast = Ast::FunctionCall {
2949 name: "test_func".to_string(),
2950 args: vec![],
2951 };
2952 let exit_code = execute(ast, &mut shell_state);
2953 assert_eq!(exit_code, 0);
2954 }
2955
2956 #[test]
2957 fn test_execute_function_call_with_args() {
2958 let mut shell_state = ShellState::new();
2960 shell_state.define_function(
2961 "test_func".to_string(),
2962 Ast::Pipeline(vec![ShellCommand {
2963 args: vec!["echo".to_string(), "arg1".to_string()],
2964 redirections: Vec::new(),
2965 compound: None,
2966 }]),
2967 );
2968
2969 let ast = Ast::FunctionCall {
2971 name: "test_func".to_string(),
2972 args: vec!["hello".to_string()],
2973 };
2974 let exit_code = execute(ast, &mut shell_state);
2975 assert_eq!(exit_code, 0);
2976 }
2977
2978 #[test]
2979 fn test_execute_nonexistent_function() {
2980 let mut shell_state = ShellState::new();
2981 let ast = Ast::FunctionCall {
2982 name: "nonexistent".to_string(),
2983 args: vec![],
2984 };
2985 let exit_code = execute(ast, &mut shell_state);
2986 assert_eq!(exit_code, 1); }
2988
2989 #[test]
2990 fn test_execute_function_integration() {
2991 let mut shell_state = ShellState::new();
2993
2994 let define_ast = Ast::FunctionDefinition {
2996 name: "hello".to_string(),
2997 body: Box::new(Ast::Pipeline(vec![ShellCommand {
2998 args: vec!["printf".to_string(), "Hello from function".to_string()],
2999 redirections: Vec::new(),
3000 compound: None,
3001 }])),
3002 };
3003 let exit_code = execute(define_ast, &mut shell_state);
3004 assert_eq!(exit_code, 0);
3005
3006 let call_ast = Ast::FunctionCall {
3008 name: "hello".to_string(),
3009 args: vec![],
3010 };
3011 let exit_code = execute(call_ast, &mut shell_state);
3012 assert_eq!(exit_code, 0);
3013 }
3014
3015 #[test]
3016 fn test_execute_function_with_local_variables() {
3017 let mut shell_state = ShellState::new();
3018
3019 shell_state.set_var("global_var", "global_value".to_string());
3021
3022 let define_ast = Ast::FunctionDefinition {
3024 name: "test_func".to_string(),
3025 body: Box::new(Ast::Sequence(vec![
3026 Ast::LocalAssignment {
3027 var: "local_var".to_string(),
3028 value: "local_value".to_string(),
3029 },
3030 Ast::Assignment {
3031 var: "global_var".to_string(),
3032 value: "modified_in_function".to_string(),
3033 },
3034 Ast::Pipeline(vec![ShellCommand {
3035 args: vec!["printf".to_string(), "success".to_string()],
3036 redirections: Vec::new(),
3037 compound: None,
3038 }]),
3039 ])),
3040 };
3041 let exit_code = execute(define_ast, &mut shell_state);
3042 assert_eq!(exit_code, 0);
3043
3044 assert_eq!(
3046 shell_state.get_var("global_var"),
3047 Some("global_value".to_string())
3048 );
3049
3050 let call_ast = Ast::FunctionCall {
3052 name: "test_func".to_string(),
3053 args: vec![],
3054 };
3055 let exit_code = execute(call_ast, &mut shell_state);
3056 assert_eq!(exit_code, 0);
3057
3058 assert_eq!(
3060 shell_state.get_var("global_var"),
3061 Some("modified_in_function".to_string())
3062 );
3063 }
3064
3065 #[test]
3066 fn test_execute_nested_function_calls() {
3067 let mut shell_state = ShellState::new();
3068
3069 shell_state.set_var("global_var", "global".to_string());
3071
3072 let outer_func = Ast::FunctionDefinition {
3074 name: "outer".to_string(),
3075 body: Box::new(Ast::Sequence(vec![
3076 Ast::Assignment {
3077 var: "global_var".to_string(),
3078 value: "outer_modified".to_string(),
3079 },
3080 Ast::FunctionCall {
3081 name: "inner".to_string(),
3082 args: vec![],
3083 },
3084 Ast::Pipeline(vec![ShellCommand {
3085 args: vec!["printf".to_string(), "outer_done".to_string()],
3086 redirections: Vec::new(),
3087 compound: None,
3088 }]),
3089 ])),
3090 };
3091
3092 let inner_func = Ast::FunctionDefinition {
3094 name: "inner".to_string(),
3095 body: Box::new(Ast::Sequence(vec![
3096 Ast::Assignment {
3097 var: "global_var".to_string(),
3098 value: "inner_modified".to_string(),
3099 },
3100 Ast::Pipeline(vec![ShellCommand {
3101 args: vec!["printf".to_string(), "inner_done".to_string()],
3102 redirections: Vec::new(),
3103 compound: None,
3104 }]),
3105 ])),
3106 };
3107
3108 execute(outer_func, &mut shell_state);
3110 execute(inner_func, &mut shell_state);
3111
3112 shell_state.set_var("global_var", "initial".to_string());
3114
3115 let call_ast = Ast::FunctionCall {
3117 name: "outer".to_string(),
3118 args: vec![],
3119 };
3120 let exit_code = execute(call_ast, &mut shell_state);
3121 assert_eq!(exit_code, 0);
3122
3123 assert_eq!(
3126 shell_state.get_var("global_var"),
3127 Some("inner_modified".to_string())
3128 );
3129 }
3130
3131 #[test]
3132 fn test_here_string_execution() {
3133 let cmd = ShellCommand {
3135 args: vec!["cat".to_string()],
3136 redirections: Vec::new(),
3137 compound: None,
3138 };
3140
3141 assert_eq!(cmd.args, vec!["cat"]);
3144 }
3146
3147 #[test]
3148 fn test_here_document_execution() {
3149 let cmd = ShellCommand {
3151 args: vec!["cat".to_string()],
3152 redirections: Vec::new(),
3153 compound: None,
3154 };
3156
3157 assert_eq!(cmd.args, vec!["cat"]);
3160 }
3162
3163 #[test]
3164 fn test_here_document_with_variable_expansion() {
3165 let mut shell_state = ShellState::new();
3167 shell_state.set_var("PWD", "/test/path".to_string());
3168
3169 let content = "Working dir: $PWD";
3171 let expanded = expand_variables_in_string(content, &mut shell_state);
3172
3173 assert_eq!(expanded, "Working dir: /test/path");
3174 }
3175
3176 #[test]
3177 fn test_here_document_with_command_substitution_builtin() {
3178 let mut shell_state = ShellState::new();
3180 shell_state.set_var("PWD", "/test/dir".to_string());
3181
3182 let content = "Current directory: `pwd`";
3184 let expanded = expand_variables_in_string(content, &mut shell_state);
3185
3186 assert!(expanded.contains("Current directory: "));
3188 }
3189
3190 #[test]
3195 fn test_fd_output_redirection() {
3196 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3197
3198 use std::time::{SystemTime, UNIX_EPOCH};
3200 let timestamp = SystemTime::now()
3201 .duration_since(UNIX_EPOCH)
3202 .unwrap()
3203 .as_nanos();
3204 let temp_file = format!("/tmp/rush_test_fd_out_{}.txt", timestamp);
3205
3206 let cmd = ShellCommand {
3208 args: vec![
3209 "sh".to_string(),
3210 "-c".to_string(),
3211 "echo error >&2".to_string(),
3212 ],
3213 redirections: vec![Redirection::FdOutput(2, temp_file.clone())],
3214 compound: None,
3215 };
3216
3217 let mut shell_state = ShellState::new();
3218 let exit_code = execute_single_command(&cmd, &mut shell_state);
3219 assert_eq!(exit_code, 0);
3220
3221 let content = std::fs::read_to_string(&temp_file).unwrap();
3223 assert_eq!(content.trim(), "error");
3224
3225 let _ = std::fs::remove_file(&temp_file);
3227 }
3228
3229 #[test]
3230 fn test_fd_input_redirection() {
3231 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3232
3233 use std::time::{SystemTime, UNIX_EPOCH};
3235 let timestamp = SystemTime::now()
3236 .duration_since(UNIX_EPOCH)
3237 .unwrap()
3238 .as_nanos();
3239 let temp_file = format!("/tmp/rush_test_fd_in_{}.txt", timestamp);
3240
3241 std::fs::write(&temp_file, "test input\n").unwrap();
3242 std::thread::sleep(std::time::Duration::from_millis(10));
3243
3244 let cmd = ShellCommand {
3247 args: vec!["cat".to_string()],
3248 compound: None,
3249 redirections: vec![
3250 Redirection::FdInput(3, temp_file.clone()),
3251 Redirection::Input(temp_file.clone()),
3252 ],
3253 };
3254
3255 let mut shell_state = ShellState::new();
3256 let exit_code = execute_single_command(&cmd, &mut shell_state);
3257 assert_eq!(exit_code, 0);
3258
3259 let _ = std::fs::remove_file(&temp_file);
3261 }
3262
3263 #[test]
3264 fn test_fd_append_redirection() {
3265 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3266
3267 use std::time::{SystemTime, UNIX_EPOCH};
3269 let timestamp = SystemTime::now()
3270 .duration_since(UNIX_EPOCH)
3271 .unwrap()
3272 .as_nanos();
3273 let temp_file = format!("/tmp/rush_test_fd_append_{}.txt", timestamp);
3274
3275 std::fs::write(&temp_file, "first line\n").unwrap();
3276 std::thread::sleep(std::time::Duration::from_millis(10));
3277
3278 let cmd = ShellCommand {
3280 args: vec![
3281 "sh".to_string(),
3282 "-c".to_string(),
3283 "echo second line >&2".to_string(),
3284 ],
3285 redirections: vec![Redirection::FdAppend(2, temp_file.clone())],
3286 compound: None,
3287 };
3288
3289 let mut shell_state = ShellState::new();
3290 let exit_code = execute_single_command(&cmd, &mut shell_state);
3291 assert_eq!(exit_code, 0);
3292
3293 let content = std::fs::read_to_string(&temp_file).unwrap();
3295 assert!(content.contains("first line"));
3296 assert!(content.contains("second line"));
3297
3298 let _ = std::fs::remove_file(&temp_file);
3300 }
3301
3302 #[test]
3303 fn test_fd_duplication_stderr_to_stdout() {
3304 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3305
3306 use std::time::{SystemTime, UNIX_EPOCH};
3308 let timestamp = SystemTime::now()
3309 .duration_since(UNIX_EPOCH)
3310 .unwrap()
3311 .as_nanos();
3312 let temp_file = format!("/tmp/rush_test_fd_dup_{}.txt", timestamp);
3313
3314 let cmd = ShellCommand {
3318 args: vec![
3319 "sh".to_string(),
3320 "-c".to_string(),
3321 "echo test; echo error >&2".to_string(),
3322 ],
3323 compound: None,
3324 redirections: vec![Redirection::Output(temp_file.clone())],
3325 };
3326
3327 let mut shell_state = ShellState::new();
3328 let exit_code = execute_single_command(&cmd, &mut shell_state);
3329 assert_eq!(exit_code, 0);
3330
3331 assert!(std::path::Path::new(&temp_file).exists());
3333 let content = std::fs::read_to_string(&temp_file).unwrap();
3334 assert!(content.contains("test"));
3335
3336 let _ = std::fs::remove_file(&temp_file);
3338 }
3339
3340 #[test]
3341 fn test_fd_close() {
3342 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3343
3344 let cmd = ShellCommand {
3346 args: vec!["sh".to_string(), "-c".to_string(), "echo test".to_string()],
3347 redirections: vec![Redirection::FdClose(2)],
3348 compound: None,
3349 };
3350
3351 let mut shell_state = ShellState::new();
3352 let exit_code = execute_single_command(&cmd, &mut shell_state);
3353 assert_eq!(exit_code, 0);
3354
3355 assert!(shell_state.fd_table.borrow().is_closed(2));
3357 }
3358
3359 #[test]
3360 fn test_fd_read_write() {
3361 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3362
3363 use std::time::{SystemTime, UNIX_EPOCH};
3365 let timestamp = SystemTime::now()
3366 .duration_since(UNIX_EPOCH)
3367 .unwrap()
3368 .as_nanos();
3369 let temp_file = format!("/tmp/rush_test_fd_rw_{}.txt", timestamp);
3370
3371 std::fs::write(&temp_file, "initial content\n").unwrap();
3372 std::thread::sleep(std::time::Duration::from_millis(10));
3373
3374 let cmd = ShellCommand {
3376 args: vec!["cat".to_string()],
3377 compound: None,
3378 redirections: vec![
3379 Redirection::FdInputOutput(3, temp_file.clone()),
3380 Redirection::Input(temp_file.clone()),
3381 ],
3382 };
3383
3384 let mut shell_state = ShellState::new();
3385 let exit_code = execute_single_command(&cmd, &mut shell_state);
3386 assert_eq!(exit_code, 0);
3387
3388 let _ = std::fs::remove_file(&temp_file);
3390 }
3391
3392 #[test]
3393 fn test_multiple_fd_redirections() {
3394 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3395
3396 use std::time::{SystemTime, UNIX_EPOCH};
3398 let timestamp = SystemTime::now()
3399 .duration_since(UNIX_EPOCH)
3400 .unwrap()
3401 .as_nanos();
3402 let out_file = format!("/tmp/rush_test_fd_multi_out_{}.txt", timestamp);
3403 let err_file = format!("/tmp/rush_test_fd_multi_err_{}.txt", timestamp);
3404
3405 let cmd = ShellCommand {
3407 args: vec![
3408 "sh".to_string(),
3409 "-c".to_string(),
3410 "echo stdout; echo stderr >&2".to_string(),
3411 ],
3412 redirections: vec![
3413 Redirection::FdOutput(2, err_file.clone()),
3414 Redirection::Output(out_file.clone()),
3415 ],
3416 compound: None,
3417 };
3418
3419 let mut shell_state = ShellState::new();
3420 let exit_code = execute_single_command(&cmd, &mut shell_state);
3421 assert_eq!(exit_code, 0);
3422
3423 assert!(std::path::Path::new(&out_file).exists());
3425 assert!(std::path::Path::new(&err_file).exists());
3426
3427 let out_content = std::fs::read_to_string(&out_file).unwrap();
3429 let err_content = std::fs::read_to_string(&err_file).unwrap();
3430 assert!(out_content.contains("stdout"));
3431 assert!(err_content.contains("stderr"));
3432
3433 let _ = std::fs::remove_file(&out_file);
3435 let _ = std::fs::remove_file(&err_file);
3436 }
3437
3438 #[test]
3439 fn test_fd_swap_pattern() {
3440 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3441
3442 use std::time::{SystemTime, UNIX_EPOCH};
3444 let timestamp = SystemTime::now()
3445 .duration_since(UNIX_EPOCH)
3446 .unwrap()
3447 .as_nanos();
3448 let temp_file = format!("/tmp/rush_test_fd_swap_{}.txt", timestamp);
3449
3450 let cmd = ShellCommand {
3453 args: vec!["sh".to_string(), "-c".to_string(), "echo test".to_string()],
3454 redirections: vec![
3455 Redirection::FdOutput(3, temp_file.clone()), Redirection::FdClose(3), Redirection::Output(temp_file.clone()), ],
3459 compound: None,
3460 };
3461
3462 let mut shell_state = ShellState::new();
3463 let exit_code = execute_single_command(&cmd, &mut shell_state);
3464 assert_eq!(exit_code, 0);
3465
3466 assert!(shell_state.fd_table.borrow().is_closed(3));
3468
3469 let _ = std::fs::remove_file(&temp_file);
3471 }
3472
3473 #[test]
3474 fn test_fd_redirection_with_pipes() {
3475 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3476
3477 use std::time::{SystemTime, UNIX_EPOCH};
3479 let timestamp = SystemTime::now()
3480 .duration_since(UNIX_EPOCH)
3481 .unwrap()
3482 .as_nanos();
3483 let temp_file = format!("/tmp/rush_test_fd_pipe_{}.txt", timestamp);
3484
3485 let commands = vec![
3488 ShellCommand {
3489 args: vec!["echo".to_string(), "piped output".to_string()],
3490 redirections: vec![],
3491 compound: None,
3492 },
3493 ShellCommand {
3494 args: vec!["cat".to_string()],
3495 compound: None,
3496 redirections: vec![Redirection::Output(temp_file.clone())],
3497 },
3498 ];
3499
3500 let mut shell_state = ShellState::new();
3501 let exit_code = execute_pipeline(&commands, &mut shell_state);
3502 assert_eq!(exit_code, 0);
3503
3504 let content = std::fs::read_to_string(&temp_file).unwrap();
3506 assert!(content.contains("piped output"));
3507
3508 let _ = std::fs::remove_file(&temp_file);
3510 }
3511
3512 #[test]
3513 fn test_fd_error_invalid_fd_number() {
3514 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3515
3516 use std::time::{SystemTime, UNIX_EPOCH};
3518 let timestamp = SystemTime::now()
3519 .duration_since(UNIX_EPOCH)
3520 .unwrap()
3521 .as_nanos();
3522 let temp_file = format!("/tmp/rush_test_fd_invalid_{}.txt", timestamp);
3523
3524 let cmd = ShellCommand {
3526 args: vec!["echo".to_string(), "test".to_string()],
3527 compound: None,
3528 redirections: vec![Redirection::FdOutput(1025, temp_file.clone())],
3529 };
3530
3531 let mut shell_state = ShellState::new();
3532 let exit_code = execute_single_command(&cmd, &mut shell_state);
3533
3534 assert_eq!(exit_code, 1);
3536
3537 let _ = std::fs::remove_file(&temp_file);
3539 }
3540
3541 #[test]
3542 fn test_fd_error_duplicate_closed_fd() {
3543 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3544
3545 let cmd = ShellCommand {
3547 args: vec!["echo".to_string(), "test".to_string()],
3548 compound: None,
3549 redirections: vec![
3550 Redirection::FdClose(3),
3551 Redirection::FdDuplicate(2, 3), ],
3553 };
3554
3555 let mut shell_state = ShellState::new();
3556 let exit_code = execute_single_command(&cmd, &mut shell_state);
3557
3558 assert_eq!(exit_code, 1);
3560 }
3561
3562 #[test]
3563 fn test_fd_error_file_permission() {
3564 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3565
3566 let cmd = ShellCommand {
3568 args: vec!["echo".to_string(), "test".to_string()],
3569 redirections: vec![Redirection::FdOutput(2, "/proc/version".to_string())],
3570 compound: None,
3571 };
3572
3573 let mut shell_state = ShellState::new();
3574 let exit_code = execute_single_command(&cmd, &mut shell_state);
3575
3576 assert_eq!(exit_code, 1);
3578 }
3579
3580 #[test]
3581 fn test_fd_redirection_order() {
3582 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3583
3584 use std::time::{SystemTime, UNIX_EPOCH};
3586 let timestamp = SystemTime::now()
3587 .duration_since(UNIX_EPOCH)
3588 .unwrap()
3589 .as_nanos();
3590 let file1 = format!("/tmp/rush_test_fd_order1_{}.txt", timestamp);
3591 let file2 = format!("/tmp/rush_test_fd_order2_{}.txt", timestamp);
3592
3593 let cmd = ShellCommand {
3596 args: vec!["echo".to_string(), "test".to_string()],
3597 compound: None,
3598 redirections: vec![
3599 Redirection::Output(file1.clone()),
3600 Redirection::Output(file2.clone()),
3601 ],
3602 };
3603
3604 let mut shell_state = ShellState::new();
3605 let exit_code = execute_single_command(&cmd, &mut shell_state);
3606 assert_eq!(exit_code, 0);
3607
3608 let content2 = std::fs::read_to_string(&file2).unwrap();
3610 assert!(content2.contains("test"));
3611
3612 let _ = std::fs::remove_file(&file1);
3614 let _ = std::fs::remove_file(&file2);
3615 }
3616
3617 #[test]
3618 fn test_fd_builtin_with_redirection() {
3619 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3620
3621 use std::time::{SystemTime, UNIX_EPOCH};
3623 let timestamp = SystemTime::now()
3624 .duration_since(UNIX_EPOCH)
3625 .unwrap()
3626 .as_nanos();
3627 let temp_file = format!("/tmp/rush_test_fd_builtin_{}.txt", timestamp);
3628
3629 let cmd = ShellCommand {
3631 args: vec!["echo".to_string(), "builtin test".to_string()],
3632 redirections: vec![Redirection::Output(temp_file.clone())],
3633 compound: None,
3634 };
3635
3636 let mut shell_state = ShellState::new();
3637 let exit_code = execute_single_command(&cmd, &mut shell_state);
3638 assert_eq!(exit_code, 0);
3639
3640 let content = std::fs::read_to_string(&temp_file).unwrap();
3642 assert!(content.contains("builtin test"));
3643
3644 let _ = std::fs::remove_file(&temp_file);
3646 }
3647
3648 #[test]
3649 fn test_fd_variable_expansion_in_filename() {
3650 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3651
3652 use std::time::{SystemTime, UNIX_EPOCH};
3654 let timestamp = SystemTime::now()
3655 .duration_since(UNIX_EPOCH)
3656 .unwrap()
3657 .as_nanos();
3658 let temp_file = format!("/tmp/rush_test_fd_var_{}.txt", timestamp);
3659
3660 let mut shell_state = ShellState::new();
3662 shell_state.set_var("OUTFILE", temp_file.clone());
3663
3664 let cmd = ShellCommand {
3666 args: vec!["echo".to_string(), "variable test".to_string()],
3667 compound: None,
3668 redirections: vec![Redirection::Output("$OUTFILE".to_string())],
3669 };
3670
3671 let exit_code = execute_single_command(&cmd, &mut shell_state);
3672 assert_eq!(exit_code, 0);
3673
3674 let content = std::fs::read_to_string(&temp_file).unwrap();
3676 assert!(content.contains("variable test"));
3677
3678 let _ = std::fs::remove_file(&temp_file);
3680 }
3681
3682 #[test]
3687 fn test_break_in_for_loop() {
3688 let mut shell_state = ShellState::new();
3689 shell_state.set_var("output", "".to_string());
3690
3691 let ast = Ast::For {
3696 variable: "i".to_string(),
3697 items: vec!["1".to_string(), "2".to_string(), "3".to_string(), "4".to_string(), "5".to_string()],
3698 body: Box::new(Ast::Sequence(vec![
3699 Ast::Assignment {
3700 var: "output".to_string(),
3701 value: "$output$i".to_string(),
3702 },
3703 Ast::If {
3704 branches: vec![(
3705 Box::new(Ast::Pipeline(vec![ShellCommand {
3706 args: vec!["test".to_string(), "$i".to_string(), "=".to_string(), "3".to_string()],
3707 redirections: vec![],
3708 compound: None,
3709 }])),
3710 Box::new(Ast::Pipeline(vec![ShellCommand {
3711 args: vec!["break".to_string()],
3712 redirections: vec![],
3713 compound: None,
3714 }])),
3715 )],
3716 else_branch: None,
3717 },
3718 ])),
3719 };
3720
3721 let exit_code = execute(ast, &mut shell_state);
3722 assert_eq!(exit_code, 0);
3723 assert_eq!(shell_state.get_var("output"), Some("123".to_string()));
3724 }
3725
3726 #[test]
3727 fn test_continue_in_for_loop() {
3728 let mut shell_state = ShellState::new();
3729 shell_state.set_var("output", "".to_string());
3730
3731 let ast = Ast::For {
3736 variable: "i".to_string(),
3737 items: vec!["1".to_string(), "2".to_string(), "3".to_string(), "4".to_string(), "5".to_string()],
3738 body: Box::new(Ast::Sequence(vec![
3739 Ast::If {
3740 branches: vec![(
3741 Box::new(Ast::Pipeline(vec![ShellCommand {
3742 args: vec!["test".to_string(), "$i".to_string(), "=".to_string(), "3".to_string()],
3743 redirections: vec![],
3744 compound: None,
3745 }])),
3746 Box::new(Ast::Pipeline(vec![ShellCommand {
3747 args: vec!["continue".to_string()],
3748 redirections: vec![],
3749 compound: None,
3750 }])),
3751 )],
3752 else_branch: None,
3753 },
3754 Ast::Assignment {
3755 var: "output".to_string(),
3756 value: "$output$i".to_string(),
3757 },
3758 ])),
3759 };
3760
3761 let exit_code = execute(ast, &mut shell_state);
3762 assert_eq!(exit_code, 0);
3763 assert_eq!(shell_state.get_var("output"), Some("1245".to_string()));
3764 }
3765
3766 #[test]
3767 fn test_break_in_while_loop() {
3768 let mut shell_state = ShellState::new();
3769 shell_state.set_var("i", "0".to_string());
3770 shell_state.set_var("output", "".to_string());
3771
3772 let ast = Ast::While {
3779 condition: Box::new(Ast::Pipeline(vec![ShellCommand {
3780 args: vec!["test".to_string(), "$i".to_string(), "-lt".to_string(), "10".to_string()],
3781 redirections: vec![],
3782 compound: None,
3783 }])),
3784 body: Box::new(Ast::Sequence(vec![
3785 Ast::Assignment {
3786 var: "i".to_string(),
3787 value: "$((i + 1))".to_string(),
3788 },
3789 Ast::Assignment {
3790 var: "output".to_string(),
3791 value: "$output$i".to_string(),
3792 },
3793 Ast::If {
3794 branches: vec![(
3795 Box::new(Ast::Pipeline(vec![ShellCommand {
3796 args: vec!["test".to_string(), "$i".to_string(), "=".to_string(), "5".to_string()],
3797 redirections: vec![],
3798 compound: None,
3799 }])),
3800 Box::new(Ast::Pipeline(vec![ShellCommand {
3801 args: vec!["break".to_string()],
3802 redirections: vec![],
3803 compound: None,
3804 }])),
3805 )],
3806 else_branch: None,
3807 },
3808 ])),
3809 };
3810
3811 let exit_code = execute(ast, &mut shell_state);
3812 assert_eq!(exit_code, 0);
3813 assert_eq!(shell_state.get_var("output"), Some("12345".to_string()));
3814 }
3815
3816 #[test]
3817 fn test_continue_in_while_loop() {
3818 let mut shell_state = ShellState::new();
3819 shell_state.set_var("i", "0".to_string());
3820 shell_state.set_var("output", "".to_string());
3821
3822 let ast = Ast::While {
3829 condition: Box::new(Ast::Pipeline(vec![ShellCommand {
3830 args: vec!["test".to_string(), "$i".to_string(), "-lt".to_string(), "5".to_string()],
3831 redirections: vec![],
3832 compound: None,
3833 }])),
3834 body: Box::new(Ast::Sequence(vec![
3835 Ast::Assignment {
3836 var: "i".to_string(),
3837 value: "$((i + 1))".to_string(),
3838 },
3839 Ast::If {
3840 branches: vec![(
3841 Box::new(Ast::Pipeline(vec![ShellCommand {
3842 args: vec!["test".to_string(), "$i".to_string(), "=".to_string(), "3".to_string()],
3843 redirections: vec![],
3844 compound: None,
3845 }])),
3846 Box::new(Ast::Pipeline(vec![ShellCommand {
3847 args: vec!["continue".to_string()],
3848 redirections: vec![],
3849 compound: None,
3850 }])),
3851 )],
3852 else_branch: None,
3853 },
3854 Ast::Assignment {
3855 var: "output".to_string(),
3856 value: "$output$i".to_string(),
3857 },
3858 ])),
3859 };
3860
3861 let exit_code = execute(ast, &mut shell_state);
3862 assert_eq!(exit_code, 0);
3863 assert_eq!(shell_state.get_var("output"), Some("1245".to_string()));
3864 }
3865
3866 #[test]
3867 fn test_break_nested_loops() {
3868 let mut shell_state = ShellState::new();
3869 shell_state.set_var("output", "".to_string());
3870
3871 let inner_loop = Ast::For {
3878 variable: "j".to_string(),
3879 items: vec!["a".to_string(), "b".to_string(), "c".to_string()],
3880 body: Box::new(Ast::Sequence(vec![
3881 Ast::Assignment {
3882 var: "output".to_string(),
3883 value: "$output$i$j".to_string(),
3884 },
3885 Ast::If {
3886 branches: vec![(
3887 Box::new(Ast::Pipeline(vec![ShellCommand {
3888 args: vec!["test".to_string(), "$j".to_string(), "=".to_string(), "b".to_string()],
3889 redirections: vec![],
3890 compound: None,
3891 }])),
3892 Box::new(Ast::Pipeline(vec![ShellCommand {
3893 args: vec!["break".to_string()],
3894 redirections: vec![],
3895 compound: None,
3896 }])),
3897 )],
3898 else_branch: None,
3899 },
3900 ])),
3901 };
3902
3903 let outer_loop = Ast::For {
3904 variable: "i".to_string(),
3905 items: vec!["1".to_string(), "2".to_string(), "3".to_string()],
3906 body: Box::new(inner_loop),
3907 };
3908
3909 let exit_code = execute(outer_loop, &mut shell_state);
3910 assert_eq!(exit_code, 0);
3911 assert_eq!(shell_state.get_var("output"), Some("1a1b2a2b3a3b".to_string()));
3912 }
3913
3914 #[test]
3915 fn test_break_2_nested_loops() {
3916 let mut shell_state = ShellState::new();
3917 shell_state.set_var("output", "".to_string());
3918
3919 let inner_loop = Ast::For {
3926 variable: "j".to_string(),
3927 items: vec!["a".to_string(), "b".to_string(), "c".to_string()],
3928 body: Box::new(Ast::Sequence(vec![
3929 Ast::Assignment {
3930 var: "output".to_string(),
3931 value: "$output$i$j".to_string(),
3932 },
3933 Ast::And {
3934 left: Box::new(Ast::Pipeline(vec![ShellCommand {
3935 args: vec!["test".to_string(), "$i".to_string(), "=".to_string(), "2".to_string()],
3936 redirections: vec![],
3937 compound: None,
3938 }])),
3939 right: Box::new(Ast::If {
3940 branches: vec![(
3941 Box::new(Ast::Pipeline(vec![ShellCommand {
3942 args: vec!["test".to_string(), "$j".to_string(), "=".to_string(), "b".to_string()],
3943 redirections: vec![],
3944 compound: None,
3945 }])),
3946 Box::new(Ast::Pipeline(vec![ShellCommand {
3947 args: vec!["break".to_string(), "2".to_string()],
3948 redirections: vec![],
3949 compound: None,
3950 }])),
3951 )],
3952 else_branch: None,
3953 }),
3954 },
3955 ])),
3956 };
3957
3958 let outer_loop = Ast::For {
3959 variable: "i".to_string(),
3960 items: vec!["1".to_string(), "2".to_string(), "3".to_string()],
3961 body: Box::new(inner_loop),
3962 };
3963
3964 let exit_code = execute(outer_loop, &mut shell_state);
3965 assert_eq!(exit_code, 0);
3966 assert_eq!(shell_state.get_var("output"), Some("1a1b1c2a2b".to_string()));
3967 }
3968
3969 #[test]
3970 fn test_continue_nested_loops() {
3971 let mut shell_state = ShellState::new();
3972 shell_state.set_var("output", "".to_string());
3973
3974 let inner_loop = Ast::For {
3981 variable: "j".to_string(),
3982 items: vec!["a".to_string(), "b".to_string(), "c".to_string()],
3983 body: Box::new(Ast::Sequence(vec![
3984 Ast::If {
3985 branches: vec![(
3986 Box::new(Ast::Pipeline(vec![ShellCommand {
3987 args: vec!["test".to_string(), "$j".to_string(), "=".to_string(), "b".to_string()],
3988 redirections: vec![],
3989 compound: None,
3990 }])),
3991 Box::new(Ast::Pipeline(vec![ShellCommand {
3992 args: vec!["continue".to_string()],
3993 redirections: vec![],
3994 compound: None,
3995 }])),
3996 )],
3997 else_branch: None,
3998 },
3999 Ast::Assignment {
4000 var: "output".to_string(),
4001 value: "$output$i$j".to_string(),
4002 },
4003 ])),
4004 };
4005
4006 let outer_loop = Ast::For {
4007 variable: "i".to_string(),
4008 items: vec!["1".to_string(), "2".to_string(), "3".to_string()],
4009 body: Box::new(inner_loop),
4010 };
4011
4012 let exit_code = execute(outer_loop, &mut shell_state);
4013 assert_eq!(exit_code, 0);
4014 assert_eq!(shell_state.get_var("output"), Some("1a1c2a2c3a3c".to_string()));
4015 }
4016
4017 #[test]
4018 fn test_continue_2_nested_loops() {
4019 let mut shell_state = ShellState::new();
4020 shell_state.set_var("output", "".to_string());
4021
4022 let inner_loop = Ast::For {
4030 variable: "j".to_string(),
4031 items: vec!["a".to_string(), "b".to_string(), "c".to_string()],
4032 body: Box::new(Ast::Sequence(vec![
4033 Ast::And {
4034 left: Box::new(Ast::Pipeline(vec![ShellCommand {
4035 args: vec!["test".to_string(), "$i".to_string(), "=".to_string(), "2".to_string()],
4036 redirections: vec![],
4037 compound: None,
4038 }])),
4039 right: Box::new(Ast::If {
4040 branches: vec![(
4041 Box::new(Ast::Pipeline(vec![ShellCommand {
4042 args: vec!["test".to_string(), "$j".to_string(), "=".to_string(), "b".to_string()],
4043 redirections: vec![],
4044 compound: None,
4045 }])),
4046 Box::new(Ast::Pipeline(vec![ShellCommand {
4047 args: vec!["continue".to_string(), "2".to_string()],
4048 redirections: vec![],
4049 compound: None,
4050 }])),
4051 )],
4052 else_branch: None,
4053 }),
4054 },
4055 Ast::Assignment {
4056 var: "output".to_string(),
4057 value: "$output$i$j".to_string(),
4058 },
4059 ])),
4060 };
4061
4062 let outer_loop = Ast::For {
4063 variable: "i".to_string(),
4064 items: vec!["1".to_string(), "2".to_string(), "3".to_string()],
4065 body: Box::new(Ast::Sequence(vec![
4066 inner_loop,
4067 Ast::Assignment {
4068 var: "output".to_string(),
4069 value: "$output$i-".to_string(),
4070 },
4071 ])),
4072 };
4073
4074 let exit_code = execute(outer_loop, &mut shell_state);
4075 assert_eq!(exit_code, 0);
4076 assert_eq!(shell_state.get_var("output"), Some("1a1b1c1-2a3a3b3c3-".to_string()));
4078 }
4079
4080 #[test]
4081 fn test_break_preserves_exit_code() {
4082 let mut shell_state = ShellState::new();
4083
4084 let ast = Ast::For {
4090 variable: "i".to_string(),
4091 items: vec!["1".to_string(), "2".to_string(), "3".to_string()],
4092 body: Box::new(Ast::Sequence(vec![
4093 Ast::Pipeline(vec![ShellCommand {
4094 args: vec!["false".to_string()],
4095 redirections: vec![],
4096 compound: None,
4097 }]),
4098 Ast::Pipeline(vec![ShellCommand {
4099 args: vec!["break".to_string()],
4100 redirections: vec![],
4101 compound: None,
4102 }]),
4103 ])),
4104 };
4105
4106 let exit_code = execute(ast, &mut shell_state);
4107 assert_eq!(exit_code, 0);
4109 }
4110
4111 #[test]
4112 fn test_continue_preserves_exit_code() {
4113 let mut shell_state = ShellState::new();
4114 shell_state.set_var("count", "0".to_string());
4115
4116 let ast = Ast::For {
4122 variable: "i".to_string(),
4123 items: vec!["1".to_string(), "2".to_string()],
4124 body: Box::new(Ast::Sequence(vec![
4125 Ast::Assignment {
4126 var: "count".to_string(),
4127 value: "$((count + 1))".to_string(),
4128 },
4129 Ast::Pipeline(vec![ShellCommand {
4130 args: vec!["false".to_string()],
4131 redirections: vec![],
4132 compound: None,
4133 }]),
4134 Ast::Pipeline(vec![ShellCommand {
4135 args: vec!["continue".to_string()],
4136 redirections: vec![],
4137 compound: None,
4138 }]),
4139 ])),
4140 };
4141
4142 let exit_code = execute(ast, &mut shell_state);
4143 assert_eq!(exit_code, 0);
4145 assert_eq!(shell_state.get_var("count"), Some("2".to_string()));
4146 }
4147
4148 #[test]
4153 fn test_until_basic_loop() {
4154 let mut shell_state = ShellState::new();
4155 shell_state.set_var("i", "0".to_string());
4156 shell_state.set_var("output", "".to_string());
4157
4158 let ast = Ast::Until {
4160 condition: Box::new(Ast::Pipeline(vec![ShellCommand {
4161 args: vec!["test".to_string(), "$i".to_string(), "=".to_string(), "3".to_string()],
4162 redirections: vec![],
4163 compound: None,
4164 }])),
4165 body: Box::new(Ast::Sequence(vec![
4166 Ast::Assignment {
4167 var: "output".to_string(),
4168 value: "$output$i".to_string(),
4169 },
4170 Ast::Assignment {
4171 var: "i".to_string(),
4172 value: "$((i + 1))".to_string(),
4173 },
4174 ])),
4175 };
4176
4177 let exit_code = execute(ast, &mut shell_state);
4178 assert_eq!(exit_code, 0);
4179 assert_eq!(shell_state.get_var("output"), Some("012".to_string()));
4180 assert_eq!(shell_state.get_var("i"), Some("3".to_string()));
4181 }
4182
4183 #[test]
4184 fn test_until_condition_initially_true() {
4185 let mut shell_state = ShellState::new();
4186 shell_state.set_var("executed", "no".to_string());
4187
4188 let ast = Ast::Until {
4190 condition: Box::new(Ast::Pipeline(vec![ShellCommand {
4191 args: vec!["true".to_string()],
4192 redirections: vec![],
4193 compound: None,
4194 }])),
4195 body: Box::new(Ast::Assignment {
4196 var: "executed".to_string(),
4197 value: "yes".to_string(),
4198 }),
4199 };
4200
4201 let exit_code = execute(ast, &mut shell_state);
4202 assert_eq!(exit_code, 0);
4203 assert_eq!(shell_state.get_var("executed"), Some("no".to_string()));
4205 }
4206
4207 #[test]
4208 fn test_until_with_commands_in_body() {
4209 let mut shell_state = ShellState::new();
4210 shell_state.set_var("count", "0".to_string());
4211
4212 let ast = Ast::Until {
4214 condition: Box::new(Ast::Pipeline(vec![ShellCommand {
4215 args: vec!["test".to_string(), "$count".to_string(), "-ge".to_string(), "3".to_string()],
4216 redirections: vec![],
4217 compound: None,
4218 }])),
4219 body: Box::new(Ast::Sequence(vec![
4220 Ast::Assignment {
4221 var: "count".to_string(),
4222 value: "$((count + 1))".to_string(),
4223 },
4224 Ast::Pipeline(vec![ShellCommand {
4225 args: vec!["echo".to_string(), "$count".to_string()],
4226 redirections: vec![],
4227 compound: None,
4228 }]),
4229 ])),
4230 };
4231
4232 let exit_code = execute(ast, &mut shell_state);
4233 assert_eq!(exit_code, 0);
4234 assert_eq!(shell_state.get_var("count"), Some("3".to_string()));
4235 }
4236
4237 #[test]
4238 fn test_until_with_variable_modification() {
4239 let mut shell_state = ShellState::new();
4240 shell_state.set_var("x", "1".to_string());
4241
4242 let ast = Ast::Until {
4244 condition: Box::new(Ast::Pipeline(vec![ShellCommand {
4245 args: vec!["test".to_string(), "$x".to_string(), "-gt".to_string(), "5".to_string()],
4246 redirections: vec![],
4247 compound: None,
4248 }])),
4249 body: Box::new(Ast::Assignment {
4250 var: "x".to_string(),
4251 value: "$((x * 2))".to_string(),
4252 }),
4253 };
4254
4255 let exit_code = execute(ast, &mut shell_state);
4256 assert_eq!(exit_code, 0);
4257 assert_eq!(shell_state.get_var("x"), Some("8".to_string()));
4258 }
4259
4260 #[test]
4261 fn test_until_nested_loops() {
4262 let mut shell_state = ShellState::new();
4263 shell_state.set_var("output", "".to_string());
4264 shell_state.set_var("i", "0".to_string());
4265
4266 let inner_loop = Ast::Until {
4267 condition: Box::new(Ast::Pipeline(vec![ShellCommand {
4268 args: vec!["test".to_string(), "$j".to_string(), "=".to_string(), "2".to_string()],
4269 redirections: vec![],
4270 compound: None,
4271 }])),
4272 body: Box::new(Ast::Sequence(vec![
4273 Ast::Assignment {
4274 var: "output".to_string(),
4275 value: "$output$i$j".to_string(),
4276 },
4277 Ast::Assignment {
4278 var: "j".to_string(),
4279 value: "$((j + 1))".to_string(),
4280 },
4281 ])),
4282 };
4283
4284 let outer_loop = Ast::Until {
4285 condition: Box::new(Ast::Pipeline(vec![ShellCommand {
4286 args: vec!["test".to_string(), "$i".to_string(), "=".to_string(), "2".to_string()],
4287 redirections: vec![],
4288 compound: None,
4289 }])),
4290 body: Box::new(Ast::Sequence(vec![
4291 Ast::Assignment {
4292 var: "i".to_string(),
4293 value: "$((i + 1))".to_string(),
4294 },
4295 Ast::Assignment {
4296 var: "j".to_string(),
4297 value: "0".to_string(),
4298 },
4299 inner_loop,
4300 ])),
4301 };
4302
4303 let exit_code = execute(outer_loop, &mut shell_state);
4304 assert_eq!(exit_code, 0);
4305 assert_eq!(shell_state.get_var("output"), Some("10112021".to_string()));
4306 }
4307
4308 #[test]
4309 fn test_until_with_break() {
4310 let mut shell_state = ShellState::new();
4311 shell_state.set_var("i", "0".to_string());
4312 shell_state.set_var("output", "".to_string());
4313
4314 let ast = Ast::Until {
4315 condition: Box::new(Ast::Pipeline(vec![ShellCommand {
4316 args: vec!["false".to_string()],
4317 redirections: vec![],
4318 compound: None,
4319 }])),
4320 body: Box::new(Ast::Sequence(vec![
4321 Ast::Assignment {
4322 var: "output".to_string(),
4323 value: "$output$i".to_string(),
4324 },
4325 Ast::Assignment {
4326 var: "i".to_string(),
4327 value: "$((i + 1))".to_string(),
4328 },
4329 Ast::If {
4330 branches: vec![(
4331 Box::new(Ast::Pipeline(vec![ShellCommand {
4332 args: vec!["test".to_string(), "$i".to_string(), "=".to_string(), "3".to_string()],
4333 redirections: vec![],
4334 compound: None,
4335 }])),
4336 Box::new(Ast::Pipeline(vec![ShellCommand {
4337 args: vec!["break".to_string()],
4338 redirections: vec![],
4339 compound: None,
4340 }])),
4341 )],
4342 else_branch: None,
4343 },
4344 ])),
4345 };
4346
4347 let exit_code = execute(ast, &mut shell_state);
4348 assert_eq!(exit_code, 0);
4349 assert_eq!(shell_state.get_var("output"), Some("012".to_string()));
4350 }
4351
4352 #[test]
4353 fn test_until_with_continue() {
4354 let mut shell_state = ShellState::new();
4355 shell_state.set_var("i", "0".to_string());
4356 shell_state.set_var("output", "".to_string());
4357
4358 let ast = Ast::Until {
4359 condition: Box::new(Ast::Pipeline(vec![ShellCommand {
4360 args: vec!["test".to_string(), "$i".to_string(), "-ge".to_string(), "5".to_string()],
4361 redirections: vec![],
4362 compound: None,
4363 }])),
4364 body: Box::new(Ast::Sequence(vec![
4365 Ast::Assignment {
4366 var: "i".to_string(),
4367 value: "$((i + 1))".to_string(),
4368 },
4369 Ast::If {
4370 branches: vec![(
4371 Box::new(Ast::Pipeline(vec![ShellCommand {
4372 args: vec!["test".to_string(), "$i".to_string(), "=".to_string(), "3".to_string()],
4373 redirections: vec![],
4374 compound: None,
4375 }])),
4376 Box::new(Ast::Pipeline(vec![ShellCommand {
4377 args: vec!["continue".to_string()],
4378 redirections: vec![],
4379 compound: None,
4380 }])),
4381 )],
4382 else_branch: None,
4383 },
4384 Ast::Assignment {
4385 var: "output".to_string(),
4386 value: "$output$i".to_string(),
4387 },
4388 ])),
4389 };
4390
4391 let exit_code = execute(ast, &mut shell_state);
4392 assert_eq!(exit_code, 0);
4393 assert_eq!(shell_state.get_var("output"), Some("1245".to_string()));
4394 }
4395
4396 #[test]
4397 fn test_until_empty_body() {
4398 let mut shell_state = ShellState::new();
4399 shell_state.set_var("i", "0".to_string());
4400
4401 let ast = Ast::Until {
4403 condition: Box::new(Ast::Pipeline(vec![ShellCommand {
4404 args: vec!["true".to_string()],
4405 redirections: vec![],
4406 compound: None,
4407 }])),
4408 body: Box::new(Ast::Pipeline(vec![ShellCommand {
4409 args: vec!["true".to_string()],
4410 redirections: vec![],
4411 compound: None,
4412 }])),
4413 };
4414
4415 let exit_code = execute(ast, &mut shell_state);
4416 assert_eq!(exit_code, 0);
4417 }
4418
4419 #[test]
4420 fn test_until_with_command_substitution() {
4421 let mut shell_state = ShellState::new();
4422 shell_state.set_var("count", "0".to_string());
4423 shell_state.set_var("output", "".to_string());
4424
4425 let ast = Ast::Until {
4427 condition: Box::new(Ast::Pipeline(vec![ShellCommand {
4428 args: vec!["test".to_string(), "$(echo $count)".to_string(), "=".to_string(), "3".to_string()],
4429 redirections: vec![],
4430 compound: None,
4431 }])),
4432 body: Box::new(Ast::Sequence(vec![
4433 Ast::Assignment {
4434 var: "output".to_string(),
4435 value: "$output$count".to_string(),
4436 },
4437 Ast::Assignment {
4438 var: "count".to_string(),
4439 value: "$((count + 1))".to_string(),
4440 },
4441 ])),
4442 };
4443
4444 let exit_code = execute(ast, &mut shell_state);
4445 assert_eq!(exit_code, 0);
4446 assert_eq!(shell_state.get_var("output"), Some("012".to_string()));
4447 }
4448
4449 #[test]
4450 fn test_until_with_arithmetic_condition() {
4451 let mut shell_state = ShellState::new();
4452 shell_state.set_var("x", "1".to_string());
4453 shell_state.set_var("output", "".to_string());
4454
4455 let ast = Ast::Until {
4457 condition: Box::new(Ast::Pipeline(vec![ShellCommand {
4458 args: vec!["test".to_string(), "$((x * 2))".to_string(), "-gt".to_string(), "10".to_string()],
4459 redirections: vec![],
4460 compound: None,
4461 }])),
4462 body: Box::new(Ast::Sequence(vec![
4463 Ast::Assignment {
4464 var: "output".to_string(),
4465 value: "$output$x".to_string(),
4466 },
4467 Ast::Assignment {
4468 var: "x".to_string(),
4469 value: "$((x + 1))".to_string(),
4470 },
4471 ])),
4472 };
4473
4474 let exit_code = execute(ast, &mut shell_state);
4475 assert_eq!(exit_code, 0);
4476 assert_eq!(shell_state.get_var("output"), Some("12345".to_string()));
4477 }
4478
4479 #[test]
4480 fn test_until_inside_for() {
4481 let mut shell_state = ShellState::new();
4482 shell_state.set_var("output", "".to_string());
4483
4484 let inner_until = Ast::Until {
4486 condition: Box::new(Ast::Pipeline(vec![ShellCommand {
4487 args: vec!["test".to_string(), "$j".to_string(), "=".to_string(), "2".to_string()],
4488 redirections: vec![],
4489 compound: None,
4490 }])),
4491 body: Box::new(Ast::Sequence(vec![
4492 Ast::Assignment {
4493 var: "output".to_string(),
4494 value: "$output$i$j".to_string(),
4495 },
4496 Ast::Assignment {
4497 var: "j".to_string(),
4498 value: "$((j + 1))".to_string(),
4499 },
4500 ])),
4501 };
4502
4503 let outer_for = Ast::For {
4504 variable: "i".to_string(),
4505 items: vec!["1".to_string(), "2".to_string()],
4506 body: Box::new(Ast::Sequence(vec![
4507 Ast::Assignment {
4508 var: "j".to_string(),
4509 value: "0".to_string(),
4510 },
4511 inner_until,
4512 ])),
4513 };
4514
4515 let exit_code = execute(outer_for, &mut shell_state);
4516 assert_eq!(exit_code, 0);
4517 assert_eq!(shell_state.get_var("output"), Some("10112021".to_string()));
4518 }
4519
4520 #[test]
4521 fn test_for_inside_until() {
4522 let mut shell_state = ShellState::new();
4523 shell_state.set_var("output", "".to_string());
4524 shell_state.set_var("i", "0".to_string());
4525
4526 let inner_for = Ast::For {
4528 variable: "j".to_string(),
4529 items: vec!["a".to_string(), "b".to_string()],
4530 body: Box::new(Ast::Assignment {
4531 var: "output".to_string(),
4532 value: "$output$i$j".to_string(),
4533 }),
4534 };
4535
4536 let outer_until = Ast::Until {
4537 condition: Box::new(Ast::Pipeline(vec![ShellCommand {
4538 args: vec!["test".to_string(), "$i".to_string(), "=".to_string(), "2".to_string()],
4539 redirections: vec![],
4540 compound: None,
4541 }])),
4542 body: Box::new(Ast::Sequence(vec![
4543 inner_for,
4544 Ast::Assignment {
4545 var: "i".to_string(),
4546 value: "$((i + 1))".to_string(),
4547 },
4548 ])),
4549 };
4550
4551 let exit_code = execute(outer_until, &mut shell_state);
4552 assert_eq!(exit_code, 0);
4553 assert_eq!(shell_state.get_var("output"), Some("0a0b1a1b".to_string()));
4554 }
4555
4556 #[test]
4557 fn test_until_inside_while() {
4558 let mut shell_state = ShellState::new();
4559 shell_state.set_var("output", "".to_string());
4560 shell_state.set_var("i", "0".to_string());
4561
4562 let inner_until = Ast::Until {
4563 condition: Box::new(Ast::Pipeline(vec![ShellCommand {
4564 args: vec!["test".to_string(), "$j".to_string(), "=".to_string(), "2".to_string()],
4565 redirections: vec![],
4566 compound: None,
4567 }])),
4568 body: Box::new(Ast::Sequence(vec![
4569 Ast::Assignment {
4570 var: "output".to_string(),
4571 value: "$output$i$j".to_string(),
4572 },
4573 Ast::Assignment {
4574 var: "j".to_string(),
4575 value: "$((j + 1))".to_string(),
4576 },
4577 ])),
4578 };
4579
4580 let outer_while = Ast::While {
4581 condition: Box::new(Ast::Pipeline(vec![ShellCommand {
4582 args: vec!["test".to_string(), "$i".to_string(), "-lt".to_string(), "2".to_string()],
4583 redirections: vec![],
4584 compound: None,
4585 }])),
4586 body: Box::new(Ast::Sequence(vec![
4587 Ast::Assignment {
4588 var: "i".to_string(),
4589 value: "$((i + 1))".to_string(),
4590 },
4591 Ast::Assignment {
4592 var: "j".to_string(),
4593 value: "0".to_string(),
4594 },
4595 inner_until,
4596 ])),
4597 };
4598
4599 let exit_code = execute(outer_while, &mut shell_state);
4600 assert_eq!(exit_code, 0);
4601 assert_eq!(shell_state.get_var("output"), Some("10112021".to_string()));
4602 }
4603
4604 #[test]
4605 fn test_while_inside_until() {
4606 let mut shell_state = ShellState::new();
4607 shell_state.set_var("output", "".to_string());
4608 shell_state.set_var("i", "0".to_string());
4609
4610 let inner_while = Ast::While {
4611 condition: Box::new(Ast::Pipeline(vec![ShellCommand {
4612 args: vec!["test".to_string(), "$j".to_string(), "-lt".to_string(), "2".to_string()],
4613 redirections: vec![],
4614 compound: None,
4615 }])),
4616 body: Box::new(Ast::Sequence(vec![
4617 Ast::Assignment {
4618 var: "output".to_string(),
4619 value: "$output$i$j".to_string(),
4620 },
4621 Ast::Assignment {
4622 var: "j".to_string(),
4623 value: "$((j + 1))".to_string(),
4624 },
4625 ])),
4626 };
4627
4628 let outer_until = Ast::Until {
4629 condition: Box::new(Ast::Pipeline(vec![ShellCommand {
4630 args: vec!["test".to_string(), "$i".to_string(), "=".to_string(), "2".to_string()],
4631 redirections: vec![],
4632 compound: None,
4633 }])),
4634 body: Box::new(Ast::Sequence(vec![
4635 Ast::Assignment {
4636 var: "i".to_string(),
4637 value: "$((i + 1))".to_string(),
4638 },
4639 Ast::Assignment {
4640 var: "j".to_string(),
4641 value: "0".to_string(),
4642 },
4643 inner_while,
4644 ])),
4645 };
4646
4647 let exit_code = execute(outer_until, &mut shell_state);
4648 assert_eq!(exit_code, 0);
4649 assert_eq!(shell_state.get_var("output"), Some("10112021".to_string()));
4650 }
4651
4652 #[test]
4653 fn test_until_preserves_exit_code() {
4654 let mut shell_state = ShellState::new();
4655 shell_state.set_var("i", "0".to_string());
4656
4657 let ast = Ast::Until {
4659 condition: Box::new(Ast::Pipeline(vec![ShellCommand {
4660 args: vec!["test".to_string(), "$i".to_string(), "=".to_string(), "1".to_string()],
4661 redirections: vec![],
4662 compound: None,
4663 }])),
4664 body: Box::new(Ast::Sequence(vec![
4665 Ast::Assignment {
4666 var: "i".to_string(),
4667 value: "$((i + 1))".to_string(),
4668 },
4669 Ast::Pipeline(vec![ShellCommand {
4670 args: vec!["false".to_string()],
4671 redirections: vec![],
4672 compound: None,
4673 }]),
4674 ])),
4675 };
4676
4677 let exit_code = execute(ast, &mut shell_state);
4678 assert_eq!(exit_code, 1);
4680 }
4681
4682 #[test]
4687 fn test_and_with_return_in_lhs() {
4688 let mut shell_state = ShellState::new();
4689 shell_state.set_var("executed", "no".to_string());
4690
4691 shell_state.define_function(
4693 "early_return".to_string(),
4694 Ast::Sequence(vec![
4695 Ast::Assignment {
4696 var: "executed".to_string(),
4697 value: "yes".to_string(),
4698 },
4699 Ast::Return { value: Some("5".to_string()) },
4700 ]),
4701 );
4702
4703 let ast = Ast::FunctionCall {
4705 name: "early_return".to_string(),
4706 args: vec![],
4707 };
4708
4709 let exit_code = execute(ast, &mut shell_state);
4710 assert_eq!(exit_code, 5);
4711 assert_eq!(shell_state.get_var("executed"), Some("yes".to_string()));
4712 }
4713
4714 #[test]
4715 fn test_and_with_exit_in_lhs() {
4716 let mut shell_state = ShellState::new();
4717 shell_state.set_var("rhs_executed", "no".to_string());
4718
4719 let ast = Ast::And {
4721 left: Box::new(Ast::Pipeline(vec![ShellCommand {
4722 args: vec!["exit".to_string(), "42".to_string()],
4723 redirections: vec![],
4724 compound: None,
4725 }])),
4726 right: Box::new(Ast::Assignment {
4727 var: "rhs_executed".to_string(),
4728 value: "yes".to_string(),
4729 }),
4730 };
4731
4732 let exit_code = execute(ast, &mut shell_state);
4733 assert_eq!(exit_code, 42);
4734 assert_eq!(shell_state.get_var("rhs_executed"), Some("no".to_string()));
4735 assert!(shell_state.exit_requested);
4736 }
4737
4738 #[test]
4739 fn test_and_with_break_in_lhs() {
4740 let mut shell_state = ShellState::new();
4741 shell_state.set_var("output", "".to_string());
4742
4743 let ast = Ast::For {
4747 variable: "i".to_string(),
4748 items: vec!["1".to_string(), "2".to_string(), "3".to_string()],
4749 body: Box::new(Ast::And {
4750 left: Box::new(Ast::And {
4751 left: Box::new(Ast::Pipeline(vec![ShellCommand {
4752 args: vec!["break".to_string()],
4753 redirections: vec![],
4754 compound: None,
4755 }])),
4756 right: Box::new(Ast::Assignment {
4757 var: "output".to_string(),
4758 value: "${output}bad".to_string(),
4759 }),
4760 }),
4761 right: Box::new(Ast::Assignment {
4762 var: "output".to_string(),
4763 value: "${output}$i".to_string(),
4764 }),
4765 }),
4766 };
4767
4768 let exit_code = execute(ast, &mut shell_state);
4769 assert_eq!(exit_code, 0);
4770 assert_eq!(shell_state.get_var("output"), Some("".to_string()));
4772 }
4773
4774 #[test]
4775 fn test_and_with_continue_in_lhs() {
4776 let mut shell_state = ShellState::new();
4777 shell_state.set_var("output", "".to_string());
4778
4779 let ast = Ast::For {
4784 variable: "i".to_string(),
4785 items: vec!["1".to_string(), "2".to_string(), "3".to_string()],
4786 body: Box::new(Ast::Sequence(vec![
4787 Ast::And {
4788 left: Box::new(Ast::Pipeline(vec![ShellCommand {
4789 args: vec!["continue".to_string()],
4790 redirections: vec![],
4791 compound: None,
4792 }])),
4793 right: Box::new(Ast::Assignment {
4794 var: "output".to_string(),
4795 value: "${output}bad".to_string(),
4796 }),
4797 },
4798 Ast::Assignment {
4799 var: "output".to_string(),
4800 value: "${output}$i".to_string(),
4801 },
4802 ])),
4803 };
4804
4805 let exit_code = execute(ast, &mut shell_state);
4806 assert_eq!(exit_code, 0);
4807 assert_eq!(shell_state.get_var("output"), Some("".to_string()));
4809 }
4810
4811 #[test]
4812 fn test_or_with_return_in_lhs() {
4813 let mut shell_state = ShellState::new();
4814 shell_state.set_var("executed", "no".to_string());
4815
4816 shell_state.define_function(
4818 "early_return".to_string(),
4819 Ast::Sequence(vec![
4820 Ast::Assignment {
4821 var: "executed".to_string(),
4822 value: "yes".to_string(),
4823 },
4824 Ast::Return { value: Some("5".to_string()) },
4825 ]),
4826 );
4827
4828 let ast = Ast::FunctionCall {
4830 name: "early_return".to_string(),
4831 args: vec![],
4832 };
4833
4834 let exit_code = execute(ast, &mut shell_state);
4835 assert_eq!(exit_code, 5);
4836 assert_eq!(shell_state.get_var("executed"), Some("yes".to_string()));
4837 }
4838
4839 #[test]
4840 fn test_or_with_exit_in_lhs() {
4841 let mut shell_state = ShellState::new();
4842 shell_state.set_var("rhs_executed", "no".to_string());
4843
4844 let ast = Ast::Or {
4846 left: Box::new(Ast::Pipeline(vec![ShellCommand {
4847 args: vec!["exit".to_string(), "42".to_string()],
4848 redirections: vec![],
4849 compound: None,
4850 }])),
4851 right: Box::new(Ast::Assignment {
4852 var: "rhs_executed".to_string(),
4853 value: "yes".to_string(),
4854 }),
4855 };
4856
4857 let exit_code = execute(ast, &mut shell_state);
4858 assert_eq!(exit_code, 42);
4859 assert_eq!(shell_state.get_var("rhs_executed"), Some("no".to_string()));
4860 assert!(shell_state.exit_requested);
4861 }
4862
4863 #[test]
4864 fn test_or_with_break_in_lhs() {
4865 let mut shell_state = ShellState::new();
4866 shell_state.set_var("output", "".to_string());
4867
4868 let ast = Ast::For {
4872 variable: "i".to_string(),
4873 items: vec!["1".to_string(), "2".to_string(), "3".to_string()],
4874 body: Box::new(Ast::Or {
4875 left: Box::new(Ast::Or {
4876 left: Box::new(Ast::Pipeline(vec![ShellCommand {
4877 args: vec!["false".to_string()],
4878 redirections: vec![],
4879 compound: None,
4880 }])),
4881 right: Box::new(Ast::Pipeline(vec![ShellCommand {
4882 args: vec!["break".to_string()],
4883 redirections: vec![],
4884 compound: None,
4885 }])),
4886 }),
4887 right: Box::new(Ast::Assignment {
4888 var: "output".to_string(),
4889 value: "${output}$i".to_string(),
4890 }),
4891 }),
4892 };
4893
4894 let exit_code = execute(ast, &mut shell_state);
4895 assert_eq!(exit_code, 0);
4896 assert_eq!(shell_state.get_var("output"), Some("".to_string()));
4898 }
4899
4900 #[test]
4901 fn test_or_with_continue_in_lhs() {
4902 let mut shell_state = ShellState::new();
4903 shell_state.set_var("output", "".to_string());
4904
4905 let ast = Ast::For {
4910 variable: "i".to_string(),
4911 items: vec!["1".to_string(), "2".to_string(), "3".to_string()],
4912 body: Box::new(Ast::Sequence(vec![
4913 Ast::Or {
4914 left: Box::new(Ast::Pipeline(vec![ShellCommand {
4915 args: vec!["false".to_string()],
4916 redirections: vec![],
4917 compound: None,
4918 }])),
4919 right: Box::new(Ast::Pipeline(vec![ShellCommand {
4920 args: vec!["continue".to_string()],
4921 redirections: vec![],
4922 compound: None,
4923 }])),
4924 },
4925 Ast::Assignment {
4926 var: "output".to_string(),
4927 value: "${output}$i".to_string(),
4928 },
4929 ])),
4930 };
4931
4932 let exit_code = execute(ast, &mut shell_state);
4933 assert_eq!(exit_code, 0);
4934 assert_eq!(shell_state.get_var("output"), Some("".to_string()));
4936 }
4937
4938 #[test]
4939 fn test_logical_chain_flag_cleanup() {
4940 let mut shell_state = ShellState::new();
4941
4942 assert!(!shell_state.in_logical_chain);
4944
4945 let ast = Ast::And {
4947 left: Box::new(Ast::Pipeline(vec![ShellCommand {
4948 args: vec!["true".to_string()],
4949 redirections: vec![],
4950 compound: None,
4951 }])),
4952 right: Box::new(Ast::Pipeline(vec![ShellCommand {
4953 args: vec!["true".to_string()],
4954 redirections: vec![],
4955 compound: None,
4956 }])),
4957 };
4958
4959 execute(ast, &mut shell_state);
4960
4961 assert!(!shell_state.in_logical_chain);
4963 }
4964
4965 #[test]
4966 fn test_logical_chain_flag_cleanup_with_return() {
4967 let mut shell_state = ShellState::new();
4968
4969 shell_state.define_function(
4971 "test_return".to_string(),
4972 Ast::Return { value: Some("0".to_string()) },
4973 );
4974
4975 let ast = Ast::And {
4977 left: Box::new(Ast::FunctionCall {
4978 name: "test_return".to_string(),
4979 args: vec![],
4980 }),
4981 right: Box::new(Ast::Pipeline(vec![ShellCommand {
4982 args: vec!["echo".to_string(), "should not execute".to_string()],
4983 redirections: vec![],
4984 compound: None,
4985 }])),
4986 };
4987
4988 shell_state.enter_function();
4990 execute(ast, &mut shell_state);
4991 shell_state.exit_function();
4992
4993 assert!(!shell_state.in_logical_chain);
4995 }
4996}