rush_sh/
executor.rs

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
12/// Maximum allowed subshell nesting depth to prevent stack overflow
13const MAX_SUBSHELL_DEPTH: usize = 100;
14
15/// Execute a command and capture its output as a string
16/// This is used for command substitution $(...)
17fn execute_and_capture_output(ast: Ast, shell_state: &mut ShellState) -> Result<String, String> {
18    // Create a pipe to capture stdout
19    let (reader, writer) = pipe().map_err(|e| format!("Failed to create pipe: {}", e))?;
20
21    // We need to capture the output, so we'll redirect stdout to our pipe
22    // For builtins, we can pass the writer directly
23    // For external commands, we need to handle them specially
24
25    match &ast {
26        Ast::Pipeline(commands) => {
27            // Handle both single commands and multi-command pipelines
28            if commands.is_empty() {
29                return Ok(String::new());
30            }
31
32            if commands.len() == 1 {
33                // Single command - use the existing optimized path
34                let cmd = &commands[0];
35                if cmd.args.is_empty() {
36                    return Ok(String::new());
37                }
38
39                // Expand variables and wildcards
40                let var_expanded_args = expand_variables_in_args(&cmd.args, shell_state);
41                let expanded_args = expand_wildcards(&var_expanded_args)
42                    .map_err(|e| format!("Wildcard expansion failed: {}", e))?;
43
44                if expanded_args.is_empty() {
45                    return Ok(String::new());
46                }
47
48                // Check if it's a function call
49                if shell_state.get_function(&expanded_args[0]).is_some() {
50                    // Save previous capture state (for nested command substitutions)
51                    let previous_capture = shell_state.capture_output.clone();
52
53                    // Enable output capture mode
54                    let capture_buffer = Rc::new(RefCell::new(Vec::new()));
55                    shell_state.capture_output = Some(capture_buffer.clone());
56
57                    // Create a FunctionCall AST and execute it
58                    let function_call_ast = Ast::FunctionCall {
59                        name: expanded_args[0].clone(),
60                        args: expanded_args[1..].to_vec(),
61                    };
62
63                    let exit_code = execute(function_call_ast, shell_state);
64
65                    // Retrieve captured output
66                    let captured = capture_buffer.borrow().clone();
67                    let output = String::from_utf8_lossy(&captured).trim_end().to_string();
68
69                    // Restore previous capture state
70                    shell_state.capture_output = previous_capture;
71
72                    if exit_code == 0 {
73                        Ok(output)
74                    } else {
75                        Err(format!("Function failed with exit code {}", exit_code))
76                    }
77                } else if crate::builtins::is_builtin(&expanded_args[0]) {
78                    let temp_cmd = ShellCommand {
79                        args: expanded_args,
80                        redirections: cmd.redirections.clone(),
81                        compound: None,
82                    };
83
84                    // Execute builtin with our writer
85                    let exit_code = crate::builtins::execute_builtin(
86                        &temp_cmd,
87                        shell_state,
88                        Some(Box::new(writer)),
89                    );
90
91                    // Read the captured output
92                    drop(temp_cmd); // Ensure writer is dropped
93                    let mut output = String::new();
94                    use std::io::Read;
95                    let mut reader = reader;
96                    reader
97                        .read_to_string(&mut output)
98                        .map_err(|e| format!("Failed to read output: {}", e))?;
99
100                    if exit_code == 0 {
101                        Ok(output.trim_end().to_string())
102                    } else {
103                        Err(format!("Command failed with exit code {}", exit_code))
104                    }
105                } else {
106                    // External command - execute with output capture
107                    drop(writer); // Close writer end before spawning
108
109                    let mut command = Command::new(&expanded_args[0]);
110                    command.args(&expanded_args[1..]);
111                    command.stdout(Stdio::piped());
112                    command.stderr(Stdio::null()); // Suppress stderr for command substitution
113
114                    // Set environment
115                    let child_env = shell_state.get_env_for_child();
116                    command.env_clear();
117                    for (key, value) in child_env {
118                        command.env(key, value);
119                    }
120
121                    let output = command
122                        .output()
123                        .map_err(|e| format!("Failed to execute command: {}", e))?;
124
125                    if output.status.success() {
126                        Ok(String::from_utf8_lossy(&output.stdout)
127                            .trim_end()
128                            .to_string())
129                    } else {
130                        Err(format!(
131                            "Command failed with exit code {}",
132                            output.status.code().unwrap_or(1)
133                        ))
134                    }
135                }
136            } else {
137                // Multi-command pipeline - execute the entire pipeline and capture output
138                drop(writer); // Close writer end before executing pipeline
139
140                // Save previous capture state (for nested command substitutions)
141                let previous_capture = shell_state.capture_output.clone();
142
143                // Enable output capture mode
144                let capture_buffer = Rc::new(RefCell::new(Vec::new()));
145                shell_state.capture_output = Some(capture_buffer.clone());
146
147                // Execute the pipeline
148                let exit_code = execute_pipeline(commands, shell_state);
149
150                // Retrieve captured output
151                let captured = capture_buffer.borrow().clone();
152                let output = String::from_utf8_lossy(&captured).trim_end().to_string();
153
154                // Restore previous capture state
155                shell_state.capture_output = previous_capture;
156
157                if exit_code == 0 {
158                    Ok(output)
159                } else {
160                    Err(format!("Pipeline failed with exit code {}", exit_code))
161                }
162            }
163        }
164        _ => {
165            // For other AST nodes (sequences, etc.), we need special handling
166            drop(writer);
167
168            // Save previous capture state
169            let previous_capture = shell_state.capture_output.clone();
170
171            // Enable output capture mode
172            let capture_buffer = Rc::new(RefCell::new(Vec::new()));
173            shell_state.capture_output = Some(capture_buffer.clone());
174
175            // Execute the AST
176            let exit_code = execute(ast, shell_state);
177
178            // Retrieve captured output
179            let captured = capture_buffer.borrow().clone();
180            let output = String::from_utf8_lossy(&captured).trim_end().to_string();
181
182            // Restore previous capture state
183            shell_state.capture_output = previous_capture;
184
185            if exit_code == 0 {
186                Ok(output)
187            } else {
188                Err(format!("Command failed with exit code {}", exit_code))
189            }
190        }
191    }
192}
193
194fn expand_variables_in_args(args: &[String], shell_state: &mut ShellState) -> Vec<String> {
195    let mut expanded_args = Vec::new();
196
197    for arg in args {
198        // Expand variables within the argument string
199        let expanded_arg = expand_variables_in_string(arg, shell_state);
200        expanded_args.push(expanded_arg);
201    }
202
203    expanded_args
204}
205
206pub fn expand_variables_in_string(input: &str, shell_state: &mut ShellState) -> String {
207    let mut result = String::new();
208    let mut chars = input.chars().peekable();
209
210    while let Some(ch) = chars.next() {
211        if ch == '$' {
212            // Check for command substitution $(...) or arithmetic expansion $((...))
213            if let Some(&'(') = chars.peek() {
214                chars.next(); // consume first (
215
216                // Check if this is arithmetic expansion $((...))
217                if let Some(&'(') = chars.peek() {
218                    // Arithmetic expansion $((...))
219                    chars.next(); // consume second (
220                    let mut arithmetic_expr = String::new();
221                    let mut paren_depth = 1;
222                    let mut found_closing = false;
223
224                    while let Some(c) = chars.next() {
225                        if c == '(' {
226                            paren_depth += 1;
227                            arithmetic_expr.push(c);
228                        } else if c == ')' {
229                            paren_depth -= 1;
230                            if paren_depth == 0 {
231                                // Found the first closing ) - check for second )
232                                if let Some(&')') = chars.peek() {
233                                    chars.next(); // consume the second )
234                                    found_closing = true;
235                                    break;
236                                } else {
237                                    // Missing second closing paren, treat as error
238                                    result.push_str("$((");
239                                    result.push_str(&arithmetic_expr);
240                                    result.push(')');
241                                    break;
242                                }
243                            }
244                            arithmetic_expr.push(c);
245                        } else {
246                            arithmetic_expr.push(c);
247                        }
248                    }
249
250                    if found_closing {
251                        // First expand variables in the arithmetic expression
252                        // The arithmetic evaluator expects variable names without $ prefix
253                        // So we need to expand $VAR to the value before evaluation
254                        let mut expanded_expr = String::new();
255                        let mut expr_chars = arithmetic_expr.chars().peekable();
256
257                        while let Some(ch) = expr_chars.next() {
258                            if ch == '$' {
259                                // Expand variable
260                                let mut var_name = String::new();
261                                if let Some(&c) = expr_chars.peek() {
262                                    if c == '?'
263                                        || c == '$'
264                                        || c == '0'
265                                        || c == '#'
266                                        || c == '*'
267                                        || c == '@'
268                                        || c.is_ascii_digit()
269                                    {
270                                        var_name.push(c);
271                                        expr_chars.next();
272                                    } else {
273                                        while let Some(&c) = expr_chars.peek() {
274                                            if c.is_alphanumeric() || c == '_' {
275                                                var_name.push(c);
276                                                expr_chars.next();
277                                            } else {
278                                                break;
279                                            }
280                                        }
281                                    }
282                                }
283
284                                if !var_name.is_empty() {
285                                    if let Some(value) = shell_state.get_var(&var_name) {
286                                        expanded_expr.push_str(&value);
287                                    } else {
288                                        // Variable not found, use 0 for arithmetic
289                                        expanded_expr.push('0');
290                                    }
291                                } else {
292                                    expanded_expr.push('$');
293                                }
294                            } else {
295                                expanded_expr.push(ch);
296                            }
297                        }
298
299                        match crate::arithmetic::evaluate_arithmetic_expression(
300                            &expanded_expr,
301                            shell_state,
302                        ) {
303                            Ok(value) => {
304                                result.push_str(&value.to_string());
305                            }
306                            Err(e) => {
307                                // On arithmetic error, display a proper error message
308                                if shell_state.colors_enabled {
309                                    result.push_str(&format!(
310                                        "{}arithmetic error: {}{}",
311                                        shell_state.color_scheme.error, e, "\x1b[0m"
312                                    ));
313                                } else {
314                                    result.push_str(&format!("arithmetic error: {}", e));
315                                }
316                            }
317                        }
318                    } else {
319                        // Didn't find proper closing - keep as literal
320                        result.push_str("$((");
321                        result.push_str(&arithmetic_expr);
322                        // Note: we don't add closing parens since they weren't in the input
323                    }
324                    continue;
325                }
326
327                // Regular command substitution $(...)
328                let mut sub_command = String::new();
329                let mut paren_depth = 1;
330
331                for c in chars.by_ref() {
332                    if c == '(' {
333                        paren_depth += 1;
334                        sub_command.push(c);
335                    } else if c == ')' {
336                        paren_depth -= 1;
337                        if paren_depth == 0 {
338                            break;
339                        }
340                        sub_command.push(c);
341                    } else {
342                        sub_command.push(c);
343                    }
344                }
345
346                // Execute the command substitution within the current shell context
347                // Parse and execute the command using our own lexer/parser/executor
348                if let Ok(tokens) = crate::lexer::lex(&sub_command, shell_state) {
349                    // Expand aliases before parsing
350                    let expanded_tokens = match crate::lexer::expand_aliases(
351                        tokens,
352                        shell_state,
353                        &mut std::collections::HashSet::new(),
354                    ) {
355                        Ok(t) => t,
356                        Err(_) => {
357                            // Alias expansion error, keep literal
358                            result.push_str("$(");
359                            result.push_str(&sub_command);
360                            result.push(')');
361                            continue;
362                        }
363                    };
364
365                    match crate::parser::parse(expanded_tokens) {
366                        Ok(ast) => {
367                            // Execute within current shell context and capture output
368                            match execute_and_capture_output(ast, shell_state) {
369                                Ok(output) => {
370                                    result.push_str(&output);
371                                }
372                                Err(_) => {
373                                    // On failure, keep the literal
374                                    result.push_str("$(");
375                                    result.push_str(&sub_command);
376                                    result.push(')');
377                                }
378                            }
379                        }
380                        Err(_parse_err) => {
381                            // Parse error - try to handle as function call if it looks like one
382                            let tokens_str = sub_command.trim();
383                            if tokens_str.contains(' ') {
384                                // Split by spaces and check if first token looks like a function call
385                                let parts: Vec<&str> = tokens_str.split_whitespace().collect();
386                                if let Some(first_token) = parts.first()
387                                    && shell_state.get_function(first_token).is_some()
388                                {
389                                    // This is a function call, create AST manually
390                                    let function_call = Ast::FunctionCall {
391                                        name: first_token.to_string(),
392                                        args: parts[1..].iter().map(|s| s.to_string()).collect(),
393                                    };
394                                    match execute_and_capture_output(function_call, shell_state) {
395                                        Ok(output) => {
396                                            result.push_str(&output);
397                                            continue;
398                                        }
399                                        Err(_) => {
400                                            // Fall back to literal
401                                        }
402                                    }
403                                }
404                            }
405                            // Keep the literal
406                            result.push_str("$(");
407                            result.push_str(&sub_command);
408                            result.push(')');
409                        }
410                    }
411                } else {
412                    // Lex error, keep literal
413                    result.push_str("$(");
414                    result.push_str(&sub_command);
415                    result.push(')');
416                }
417            } else {
418                // Regular variable
419                let mut var_name = String::new();
420                let mut next_ch = chars.peek();
421
422                // Handle special single-character variables first
423                if let Some(&c) = next_ch {
424                    if c == '?' || c == '$' || c == '0' || c == '#' || c == '*' || c == '@' {
425                        var_name.push(c);
426                        chars.next(); // consume the character
427                    } else if c.is_ascii_digit() {
428                        // Positional parameter
429                        var_name.push(c);
430                        chars.next();
431                    } else {
432                        // Regular variable name
433                        while let Some(&c) = next_ch {
434                            if c.is_alphanumeric() || c == '_' {
435                                var_name.push(c);
436                                chars.next(); // consume the character
437                                next_ch = chars.peek();
438                            } else {
439                                break;
440                            }
441                        }
442                    }
443                }
444
445                if !var_name.is_empty() {
446                    if let Some(value) = shell_state.get_var(&var_name) {
447                        result.push_str(&value);
448                    } else {
449                        // Variable not found - for positional parameters, expand to empty string
450                        // For other variables, keep the literal
451                        if var_name.chars().next().unwrap().is_ascii_digit()
452                            || var_name == "?"
453                            || var_name == "$"
454                            || var_name == "0"
455                            || var_name == "#"
456                            || var_name == "*"
457                            || var_name == "@"
458                        {
459                            // Expand to empty string for undefined positional parameters
460                        } else {
461                            // Keep the literal for regular variables
462                            result.push('$');
463                            result.push_str(&var_name);
464                        }
465                    }
466                } else {
467                    result.push('$');
468                }
469            }
470        } else if ch == '`' {
471            // Backtick command substitution
472            let mut sub_command = String::new();
473
474            for c in chars.by_ref() {
475                if c == '`' {
476                    break;
477                }
478                sub_command.push(c);
479            }
480
481            // Execute the command substitution
482            if let Ok(tokens) = crate::lexer::lex(&sub_command, shell_state) {
483                // Expand aliases before parsing
484                let expanded_tokens = match crate::lexer::expand_aliases(
485                    tokens,
486                    shell_state,
487                    &mut std::collections::HashSet::new(),
488                ) {
489                    Ok(t) => t,
490                    Err(_) => {
491                        // Alias expansion error, keep literal
492                        result.push('`');
493                        result.push_str(&sub_command);
494                        result.push('`');
495                        continue;
496                    }
497                };
498
499                if let Ok(ast) = crate::parser::parse(expanded_tokens) {
500                    // Execute and capture output
501                    match execute_and_capture_output(ast, shell_state) {
502                        Ok(output) => {
503                            result.push_str(&output);
504                        }
505                        Err(_) => {
506                            // On failure, keep the literal
507                            result.push('`');
508                            result.push_str(&sub_command);
509                            result.push('`');
510                        }
511                    }
512                } else {
513                    // Parse error, keep literal
514                    result.push('`');
515                    result.push_str(&sub_command);
516                    result.push('`');
517                }
518            } else {
519                // Lex error, keep literal
520                result.push('`');
521                result.push_str(&sub_command);
522                result.push('`');
523            }
524        } else {
525            result.push(ch);
526        }
527    }
528
529    result
530}
531
532fn expand_wildcards(args: &[String]) -> Result<Vec<String>, String> {
533    let mut expanded_args = Vec::new();
534
535    for arg in args {
536        if arg.contains('*') || arg.contains('?') || arg.contains('[') {
537            // Try to expand wildcard
538            match glob::glob(arg) {
539                Ok(paths) => {
540                    let mut matches: Vec<String> = paths
541                        .filter_map(|p| p.ok())
542                        .map(|p| p.to_string_lossy().to_string())
543                        .collect();
544                    if matches.is_empty() {
545                        // No matches, keep literal
546                        expanded_args.push(arg.clone());
547                    } else {
548                        // Sort for consistent behavior
549                        matches.sort();
550                        expanded_args.extend(matches);
551                    }
552                }
553                Err(_e) => {
554                    // Invalid pattern, keep literal
555                    expanded_args.push(arg.clone());
556                }
557            }
558        } else {
559            expanded_args.push(arg.clone());
560        }
561    }
562    Ok(expanded_args)
563}
564
565/// Collect here-document content from stdin until the specified delimiter is found
566/// This function reads from stdin line by line until it finds a line that exactly matches the delimiter
567/// If shell_state has pending_heredoc_content, it uses that instead (for script execution)
568fn collect_here_document_content(delimiter: &str, shell_state: &mut ShellState) -> String {
569    // Check if we have pending here-document content from script execution
570    if let Some(content) = shell_state.pending_heredoc_content.take() {
571        return content;
572    }
573
574    // Otherwise, read from stdin (interactive mode)
575    let stdin = std::io::stdin();
576    let mut reader = BufReader::new(stdin.lock());
577    let mut content = String::new();
578    let mut line = String::new();
579
580    loop {
581        line.clear();
582        match reader.read_line(&mut line) {
583            Ok(0) => {
584                // EOF reached
585                break;
586            }
587            Ok(_) => {
588                // Check if this line (without trailing newline) matches the delimiter
589                let line_content = line.trim_end();
590                if line_content == delimiter {
591                    // Found the delimiter, stop collecting
592                    break;
593                } else {
594                    // This is content, add it to our collection
595                    content.push_str(&line);
596                }
597            }
598            Err(e) => {
599                if shell_state.colors_enabled {
600                    eprintln!(
601                        "{}Error reading here-document content: {}\x1b[0m",
602                        shell_state.color_scheme.error, e
603                    );
604                } else {
605                    eprintln!("Error reading here-document content: {}", e);
606                }
607                break;
608            }
609        }
610    }
611
612    content
613}
614
615/// Apply all redirections for a command in left-to-right order (POSIX requirement)
616///
617/// # Arguments
618/// * `redirections` - List of redirections to apply
619/// * `shell_state` - Mutable reference to shell state
620/// * `command` - Optional mutable reference to Command (for external commands)
621///
622/// # Returns
623/// * `Ok(())` on success
624/// * `Err(String)` with error message on failure
625fn apply_redirections(
626    redirections: &[Redirection],
627    shell_state: &mut ShellState,
628    mut command: Option<&mut Command>,
629) -> Result<(), String> {
630    // Process redirections in left-to-right order per POSIX
631    for redir in redirections {
632        match redir {
633            Redirection::Input(file) => {
634                apply_input_redirection(0, file, shell_state, command.as_deref_mut())?;
635            }
636            Redirection::Output(file) => {
637                apply_output_redirection(1, file, false, shell_state, command.as_deref_mut())?;
638            }
639            Redirection::Append(file) => {
640                apply_output_redirection(1, file, true, shell_state, command.as_deref_mut())?;
641            }
642            Redirection::FdInput(fd, file) => {
643                apply_input_redirection(*fd, file, shell_state, command.as_deref_mut())?;
644            }
645            Redirection::FdOutput(fd, file) => {
646                apply_output_redirection(*fd, file, false, shell_state, command.as_deref_mut())?;
647            }
648            Redirection::FdAppend(fd, file) => {
649                apply_output_redirection(*fd, file, true, shell_state, command.as_deref_mut())?;
650            }
651            Redirection::FdDuplicate(target_fd, source_fd) => {
652                apply_fd_duplication(*target_fd, *source_fd, shell_state, command.as_deref_mut())?;
653            }
654            Redirection::FdClose(fd) => {
655                apply_fd_close(*fd, shell_state, command.as_deref_mut())?;
656            }
657            Redirection::FdInputOutput(fd, file) => {
658                apply_fd_input_output(*fd, file, shell_state, command.as_deref_mut())?;
659            }
660            Redirection::HereDoc(delimiter, quoted_str) => {
661                let quoted = quoted_str == "true";
662                apply_heredoc_redirection(
663                    0,
664                    delimiter,
665                    quoted,
666                    shell_state,
667                    command.as_deref_mut(),
668                )?;
669            }
670            Redirection::HereString(content) => {
671                apply_herestring_redirection(0, content, shell_state, command.as_deref_mut())?;
672            }
673        }
674    }
675    Ok(())
676}
677
678/// Apply input redirection for a specific file descriptor
679fn apply_input_redirection(
680    fd: i32,
681    file: &str,
682    shell_state: &mut ShellState,
683    command: Option<&mut Command>,
684) -> Result<(), String> {
685    let expanded_file = expand_variables_in_string(file, shell_state);
686
687    // Open file for reading
688    let file_handle =
689        File::open(&expanded_file).map_err(|e| format!("Cannot open {}: {}", expanded_file, e))?;
690
691    if fd == 0 {
692        // stdin redirection - apply to Command if present
693        if let Some(cmd) = command {
694            cmd.stdin(Stdio::from(file_handle));
695        } else {
696            // For builtins or command groups (command is None), redirect shell's stdin
697            shell_state.fd_table.borrow_mut().open_fd(
698                0,
699                &expanded_file,
700                true,  // read
701                false, // write
702                false, // append
703                false, // truncate
704            )?;
705
706            // Also perform OS-level dup2
707            let raw_fd = shell_state.fd_table.borrow().get_raw_fd(0);
708            if let Some(rfd) = raw_fd {
709                if rfd != 0 {
710                    unsafe {
711                        if libc::dup2(rfd, 0) < 0 {
712                            return Err(format!("Failed to dup2 fd {} to 0", rfd));
713                        }
714                    }
715                }
716            }
717        }
718    } else {
719        // Custom fd - for external commands, we need to redirect the custom fd for reading
720        // Open the file (we need to keep the handle alive for the command)
721        let fd_file = File::open(&expanded_file)
722            .map_err(|e| format!("Cannot open {}: {}", expanded_file, e))?;
723
724        // For external commands, store both in fd table and prepare for stdin redirect
725        shell_state.fd_table.borrow_mut().open_fd(
726            fd,
727            &expanded_file,
728            true,  // read
729            false, // write
730            false, // append
731            false, // truncate
732        )?;
733
734        // If we have an external command, set up the file descriptor in the child process
735        if let Some(cmd) = command {
736            // Keep fd_file alive by moving it into the closure
737            // It will be dropped (and closed) when the closure is dropped in the parent
738            let target_fd = fd;
739            unsafe {
740                cmd.pre_exec(move || {
741                    let raw_fd = fd_file.as_raw_fd();
742
743                    // The inherited file descriptor might not be at the target fd number
744                    // Use dup2 to ensure it's at the correct fd number
745                    if raw_fd != target_fd {
746                        let result = libc::dup2(raw_fd, target_fd);
747                        if result < 0 {
748                            return Err(std::io::Error::last_os_error());
749                        }
750                        // We don't need to close raw_fd manually because fd_file
751                        // has CLOEXEC set by default and will be closed on exec
752                    }
753                    Ok(())
754                });
755            }
756        }
757    }
758
759    Ok(())
760}
761
762/// Apply output redirection for a specific file descriptor
763fn apply_output_redirection(
764    fd: i32,
765    file: &str,
766    append: bool,
767    shell_state: &mut ShellState,
768    command: Option<&mut Command>,
769) -> Result<(), String> {
770    let expanded_file = expand_variables_in_string(file, shell_state);
771
772    // Open file for writing or appending
773    let file_handle = if append {
774        OpenOptions::new()
775            .append(true)
776            .create(true)
777            .open(&expanded_file)
778            .map_err(|e| format!("Cannot open {}: {}", expanded_file, e))?
779    } else {
780        File::create(&expanded_file)
781            .map_err(|e| format!("Cannot create {}: {}", expanded_file, e))?
782    };
783
784    if let Some(cmd) = command {
785        if fd == 1 {
786            // stdout redirection - apply to Command if present
787            cmd.stdout(Stdio::from(file_handle));
788        } else if fd == 2 {
789            // stderr redirection - apply to Command if present
790            cmd.stderr(Stdio::from(file_handle));
791        } else {
792            // Custom fd - store in fd table (and pre_exec will handle it?)
793            // Actually, for external commands, custom FDs need to be inherited/set up.
794            // But we can update the shell's FD table temporarily if we want?
795            // Existing logic for custom FD WAS to update fd_table.
796            shell_state.fd_table.borrow_mut().open_fd(
797                fd,
798                &expanded_file,
799                false, // read
800                true,  // write
801                append,
802                !append, // truncate if not appending
803            )?;
804        }
805    } else {
806        // Current process redirection (builtins, command groups)
807        // We MUST update the file descriptor table for ALL FDs including 1 and 2
808        shell_state.fd_table.borrow_mut().open_fd(
809            fd,
810            &expanded_file,
811            false, // read
812            true,  // write
813            append,
814            !append, // truncate if not appending
815        )?;
816
817        // Also perform OS-level dup2 to ensure child processes inherit the redirection
818        // (This is critical for external commands running inside command groups)
819        let raw_fd = shell_state.fd_table.borrow().get_raw_fd(fd);
820        if let Some(rfd) = raw_fd {
821            // Avoid dup2-ing to itself if raw_fd happens to equal fd (unlikely but possible if we closed 1 then opened)
822            if rfd != fd {
823                unsafe {
824                    if libc::dup2(rfd, fd) < 0 {
825                        return Err(format!("Failed to dup2 fd {} to {}", rfd, fd));
826                    }
827                }
828            }
829        }
830    }
831
832    Ok(())
833}
834
835/// Apply file descriptor duplication
836fn apply_fd_duplication(
837    target_fd: i32,
838    source_fd: i32,
839    shell_state: &mut ShellState,
840    _command: Option<&mut Command>,
841) -> Result<(), String> {
842    // Check if source_fd is explicitly closed before attempting duplication
843    if shell_state.fd_table.borrow().is_closed(source_fd) {
844        let error_msg = format!("File descriptor {} is closed", source_fd);
845        if shell_state.colors_enabled {
846            eprintln!(
847                "{}Redirection error: {}\x1b[0m",
848                shell_state.color_scheme.error, error_msg
849            );
850        } else {
851            eprintln!("Redirection error: {}", error_msg);
852        }
853        return Err(error_msg);
854    }
855
856    // Duplicate source_fd to target_fd
857    shell_state
858        .fd_table
859        .borrow_mut()
860        .duplicate_fd(source_fd, target_fd)?;
861    Ok(())
862}
863
864/// Apply file descriptor closing
865fn apply_fd_close(
866    fd: i32,
867    shell_state: &mut ShellState,
868    command: Option<&mut Command>,
869) -> Result<(), String> {
870    // Close the specified fd in the fd table
871    shell_state.fd_table.borrow_mut().close_fd(fd)?;
872
873    // For external commands, we need to redirect the fd to /dev/null
874    // This ensures that writes to the closed fd don't produce errors
875    if let Some(cmd) = command {
876        match fd {
877            0 => {
878                // Close stdin - redirect to /dev/null for reading
879                cmd.stdin(Stdio::null());
880            }
881            1 => {
882                // Close stdout - redirect to /dev/null for writing
883                cmd.stdout(Stdio::null());
884            }
885            2 => {
886                // Close stderr - redirect to /dev/null for writing
887                cmd.stderr(Stdio::null());
888            }
889            _ => {
890                // For custom fds (3+), we use pre_exec to close them
891                // This is handled via the fd_table and dup2 operations
892            }
893        }
894    }
895
896    Ok(())
897}
898
899/// Apply read/write file descriptor opening
900fn apply_fd_input_output(
901    fd: i32,
902    file: &str,
903    shell_state: &mut ShellState,
904    _command: Option<&mut Command>,
905) -> Result<(), String> {
906    let expanded_file = expand_variables_in_string(file, shell_state);
907
908    // Open file for both reading and writing
909    shell_state.fd_table.borrow_mut().open_fd(
910        fd,
911        &expanded_file,
912        true,  // read
913        true,  // write
914        false, // append
915        false, // truncate
916    )?;
917
918    Ok(())
919}
920
921/// Apply here-document redirection
922fn apply_heredoc_redirection(
923    fd: i32,
924    delimiter: &str,
925    quoted: bool,
926    shell_state: &mut ShellState,
927    command: Option<&mut Command>,
928) -> Result<(), String> {
929    let here_doc_content = collect_here_document_content(delimiter, shell_state);
930
931    // Expand variables and command substitutions ONLY if delimiter was not quoted
932    let expanded_content = if quoted {
933        here_doc_content
934    } else {
935        expand_variables_in_string(&here_doc_content, shell_state)
936    };
937
938    // Create a pipe and write the content
939    let (reader, mut writer) =
940        pipe().map_err(|e| format!("Failed to create pipe for here-document: {}", e))?;
941
942    writeln!(writer, "{}", expanded_content)
943        .map_err(|e| format!("Failed to write here-document content: {}", e))?;
944
945    // Apply to stdin if fd is 0
946    if fd == 0 {
947        if let Some(cmd) = command {
948            cmd.stdin(Stdio::from(reader));
949        }
950    }
951
952    Ok(())
953}
954
955/// Apply here-string redirection
956fn apply_herestring_redirection(
957    fd: i32,
958    content: &str,
959    shell_state: &mut ShellState,
960    command: Option<&mut Command>,
961) -> Result<(), String> {
962    let expanded_content = expand_variables_in_string(content, shell_state);
963
964    // Create a pipe and write the content
965    let (reader, mut writer) =
966        pipe().map_err(|e| format!("Failed to create pipe for here-string: {}", e))?;
967
968    write!(writer, "{}", expanded_content)
969        .map_err(|e| format!("Failed to write here-string content: {}", e))?;
970
971    // Apply to stdin if fd is 0
972    if fd == 0 {
973        if let Some(cmd) = command {
974            cmd.stdin(Stdio::from(reader));
975        }
976    }
977
978    Ok(())
979}
980
981/// Execute a trap handler command
982/// Note: Signal masking during trap execution will be added in a future update
983pub fn execute_trap_handler(trap_cmd: &str, shell_state: &mut ShellState) -> i32 {
984    // Save current exit code to preserve it across trap execution
985    let saved_exit_code = shell_state.last_exit_code;
986
987    // TODO: Add signal masking to prevent recursive trap calls
988    // This requires careful handling of the nix sigprocmask API
989    // For now, traps execute without signal masking
990
991    // Parse and execute the trap command
992    let result = match crate::lexer::lex(trap_cmd, shell_state) {
993        Ok(tokens) => {
994            match crate::lexer::expand_aliases(
995                tokens,
996                shell_state,
997                &mut std::collections::HashSet::new(),
998            ) {
999                Ok(expanded_tokens) => {
1000                    match crate::parser::parse(expanded_tokens) {
1001                        Ok(ast) => execute(ast, shell_state),
1002                        Err(_) => {
1003                            // Parse error in trap handler - silently continue
1004                            saved_exit_code
1005                        }
1006                    }
1007                }
1008                Err(_) => {
1009                    // Alias expansion error - silently continue
1010                    saved_exit_code
1011                }
1012            }
1013        }
1014        Err(_) => {
1015            // Lex error in trap handler - silently continue
1016            saved_exit_code
1017        }
1018    };
1019
1020    // Restore the original exit code (trap handlers don't affect $?)
1021    shell_state.last_exit_code = saved_exit_code;
1022
1023    result
1024}
1025
1026pub fn execute(ast: Ast, shell_state: &mut ShellState) -> i32 {
1027    match ast {
1028        Ast::Assignment { var, value } => {
1029            // Expand variables and command substitutions in the value
1030            let expanded_value = expand_variables_in_string(&value, shell_state);
1031            shell_state.set_var(&var, expanded_value);
1032            0
1033        }
1034        Ast::LocalAssignment { var, value } => {
1035            // Expand variables and command substitutions in the value
1036            let expanded_value = expand_variables_in_string(&value, shell_state);
1037            shell_state.set_local_var(&var, expanded_value);
1038            0
1039        }
1040        Ast::Pipeline(commands) => {
1041            if commands.is_empty() {
1042                return 0;
1043            }
1044
1045            if commands.len() == 1 {
1046                // Single command, handle redirections
1047                execute_single_command(&commands[0], shell_state)
1048            } else {
1049                // Pipeline
1050                execute_pipeline(&commands, shell_state)
1051            }
1052        }
1053        Ast::Sequence(asts) => {
1054            let mut exit_code = 0;
1055            for ast in asts {
1056                exit_code = execute(ast, shell_state);
1057
1058                // Check if we got an early return from a function
1059                if shell_state.is_returning() {
1060                    return exit_code;
1061                }
1062
1063                // Check if exit was requested (e.g., from trap handler)
1064                if shell_state.exit_requested {
1065                    return shell_state.exit_code;
1066                }
1067
1068                // Check for break/continue signals - stop executing remaining statements
1069                if shell_state.is_breaking() || shell_state.is_continuing() {
1070                    return exit_code;
1071                }
1072            }
1073            exit_code
1074        }
1075        Ast::If {
1076            branches,
1077            else_branch,
1078        } => {
1079            for (condition, then_branch) in branches {
1080                let cond_exit = execute(*condition, shell_state);
1081                if cond_exit == 0 {
1082                    let exit_code = execute(*then_branch, shell_state);
1083
1084                    // Check if we got an early return from a function
1085                    if shell_state.is_returning() {
1086                        return exit_code;
1087                    }
1088
1089                    return exit_code;
1090                }
1091            }
1092            if let Some(else_b) = else_branch {
1093                let exit_code = execute(*else_b, shell_state);
1094
1095                // Check if we got an early return from a function
1096                if shell_state.is_returning() {
1097                    return exit_code;
1098                }
1099
1100                exit_code
1101            } else {
1102                0
1103            }
1104        }
1105        Ast::Case {
1106            word,
1107            cases,
1108            default,
1109        } => {
1110            for (patterns, branch) in cases {
1111                for pattern in &patterns {
1112                    if let Ok(glob_pattern) = glob::Pattern::new(pattern) {
1113                        if glob_pattern.matches(&word) {
1114                            let exit_code = execute(branch, shell_state);
1115
1116                            // Check if we got an early return from a function
1117                            if shell_state.is_returning() {
1118                                return exit_code;
1119                            }
1120
1121                            return exit_code;
1122                        }
1123                    } else {
1124                        // If pattern is invalid, fall back to exact match
1125                        if &word == pattern {
1126                            let exit_code = execute(branch, shell_state);
1127
1128                            // Check if we got an early return from a function
1129                            if shell_state.is_returning() {
1130                                return exit_code;
1131                            }
1132
1133                            return exit_code;
1134                        }
1135                    }
1136                }
1137            }
1138            if let Some(def) = default {
1139                let exit_code = execute(*def, shell_state);
1140
1141                // Check if we got an early return from a function
1142                if shell_state.is_returning() {
1143                    return exit_code;
1144                }
1145
1146                exit_code
1147            } else {
1148                0
1149            }
1150        }
1151        Ast::For {
1152            variable,
1153            items,
1154            body,
1155        } => {
1156            let mut exit_code = 0;
1157
1158            // Enter loop context
1159            shell_state.enter_loop();
1160
1161            // Expand variables in items and perform word splitting
1162            let mut expanded_items = Vec::new();
1163            for item in items {
1164                // Expand variables in the item
1165                let expanded = expand_variables_in_string(&item, shell_state);
1166                
1167                // Perform word splitting on the expanded result
1168                // Split on whitespace (space, tab, newline)
1169                for word in expanded.split_whitespace() {
1170                    expanded_items.push(word.to_string());
1171                }
1172            }
1173
1174            // Execute the loop body for each expanded item
1175            for item in expanded_items {
1176                // Process any pending signals before executing the body
1177                crate::state::process_pending_signals(shell_state);
1178
1179                // Check if exit was requested (e.g., from trap handler)
1180                if shell_state.exit_requested {
1181                    shell_state.exit_loop();
1182                    return shell_state.exit_code;
1183                }
1184
1185                // Set the loop variable
1186                shell_state.set_var(&variable, item.clone());
1187
1188                // Execute the body
1189                exit_code = execute(*body.clone(), shell_state);
1190
1191                // Check if we got an early return from a function
1192                if shell_state.is_returning() {
1193                    shell_state.exit_loop();
1194                    return exit_code;
1195                }
1196
1197                // Check if exit was requested after executing the body
1198                if shell_state.exit_requested {
1199                    shell_state.exit_loop();
1200                    return shell_state.exit_code;
1201                }
1202
1203                // Check for break signal
1204                if shell_state.is_breaking() {
1205                    if shell_state.get_break_level() == 1 {
1206                        // Break out of this loop
1207                        shell_state.clear_break();
1208                        break;
1209                    } else {
1210                        // Decrement level and propagate to outer loop
1211                        shell_state.decrement_break_level();
1212                        break;
1213                    }
1214                }
1215
1216                // Check for continue signal
1217                if shell_state.is_continuing() {
1218                    if shell_state.get_continue_level() == 1 {
1219                        // Continue to next iteration of this loop
1220                        shell_state.clear_continue();
1221                        continue;
1222                    } else {
1223                        // Decrement level and propagate to outer loop
1224                        shell_state.decrement_continue_level();
1225                        break; // Exit this loop to continue outer loop
1226                    }
1227                }
1228            }
1229
1230            // Exit loop context
1231            shell_state.exit_loop();
1232
1233            exit_code
1234        }
1235        Ast::While { condition, body } => {
1236            let mut exit_code = 0;
1237
1238            // Enter loop context
1239            shell_state.enter_loop();
1240
1241            // Execute the loop while condition is true (exit code 0)
1242            loop {
1243                // Evaluate the condition
1244                let cond_exit = execute(*condition.clone(), shell_state);
1245
1246                // Check if we got an early return from a function
1247                if shell_state.is_returning() {
1248                    shell_state.exit_loop();
1249                    return cond_exit;
1250                }
1251
1252                // Check if exit was requested (e.g., from trap handler)
1253                if shell_state.exit_requested {
1254                    shell_state.exit_loop();
1255                    return shell_state.exit_code;
1256                }
1257
1258                // If condition is false (non-zero exit code), break
1259                if cond_exit != 0 {
1260                    break;
1261                }
1262
1263                // Execute the body
1264                exit_code = execute(*body.clone(), shell_state);
1265
1266                // Check if we got an early return from a function
1267                if shell_state.is_returning() {
1268                    shell_state.exit_loop();
1269                    return exit_code;
1270                }
1271
1272                // Check if exit was requested (e.g., from trap handler)
1273                if shell_state.exit_requested {
1274                    shell_state.exit_loop();
1275                    return shell_state.exit_code;
1276                }
1277
1278                // Check for break signal
1279                if shell_state.is_breaking() {
1280                    if shell_state.get_break_level() == 1 {
1281                        // Break out of this loop
1282                        shell_state.clear_break();
1283                        break;
1284                    } else {
1285                        // Decrement level and propagate to outer loop
1286                        shell_state.decrement_break_level();
1287                        break;
1288                    }
1289                }
1290
1291                // Check for continue signal
1292                if shell_state.is_continuing() {
1293                    if shell_state.get_continue_level() == 1 {
1294                        // Continue to next iteration of this loop
1295                        shell_state.clear_continue();
1296                        continue;
1297                    } else {
1298                        // Decrement level and propagate to outer loop
1299                        shell_state.decrement_continue_level();
1300                        break; // Exit this loop to continue outer loop
1301                    }
1302                }
1303            }
1304
1305            // Exit loop context
1306            shell_state.exit_loop();
1307
1308            exit_code
1309        }
1310        Ast::Until { condition, body } => {
1311            let mut exit_code = 0;
1312
1313            // Enter loop context
1314            shell_state.enter_loop();
1315
1316            // Execute the loop until condition is true (exit code 0)
1317            loop {
1318                // Evaluate the condition
1319                let cond_exit = execute(*condition.clone(), shell_state);
1320
1321                // Check if we got an early return from a function
1322                if shell_state.is_returning() {
1323                    shell_state.exit_loop();
1324                    return cond_exit;
1325                }
1326
1327                // Check if exit was requested (e.g., from trap handler)
1328                if shell_state.exit_requested {
1329                    shell_state.exit_loop();
1330                    return shell_state.exit_code;
1331                }
1332
1333                // If condition is true (exit code 0), break
1334                if cond_exit == 0 {
1335                    break;
1336                }
1337
1338                // Execute the body
1339                exit_code = execute(*body.clone(), shell_state);
1340
1341                // Check if we got an early return from a function
1342                if shell_state.is_returning() {
1343                    shell_state.exit_loop();
1344                    return exit_code;
1345                }
1346
1347                // Check if exit was requested (e.g., from trap handler)
1348                if shell_state.exit_requested {
1349                    shell_state.exit_loop();
1350                    return shell_state.exit_code;
1351                }
1352
1353                // Check for break signal
1354                if shell_state.is_breaking() {
1355                    if shell_state.get_break_level() == 1 {
1356                        // Break out of this loop
1357                        shell_state.clear_break();
1358                        break;
1359                    } else {
1360                        // Decrement level and propagate to outer loop
1361                        shell_state.decrement_break_level();
1362                        break;
1363                    }
1364                }
1365
1366                // Check for continue signal
1367                if shell_state.is_continuing() {
1368                    if shell_state.get_continue_level() == 1 {
1369                        // Continue to next iteration of this loop
1370                        shell_state.clear_continue();
1371                        continue;
1372                    } else {
1373                        // Decrement level and propagate to outer loop
1374                        shell_state.decrement_continue_level();
1375                        break; // Exit this loop to continue outer loop
1376                    }
1377                }
1378            }
1379
1380            // Exit loop context
1381            shell_state.exit_loop();
1382
1383            exit_code
1384        }
1385        Ast::FunctionDefinition { name, body } => {
1386            // Store function definition in shell state
1387            shell_state.define_function(name.clone(), *body);
1388            0
1389        }
1390        Ast::FunctionCall { name, args } => {
1391            if let Some(function_body) = shell_state.get_function(&name).cloned() {
1392                // Check recursion limit before entering function
1393                if shell_state.function_depth >= shell_state.max_recursion_depth {
1394                    eprintln!(
1395                        "Function recursion limit ({}) exceeded",
1396                        shell_state.max_recursion_depth
1397                    );
1398                    return 1;
1399                }
1400
1401                // Enter function context for local variable scoping
1402                shell_state.enter_function();
1403
1404                // Set up arguments as regular variables (will be enhanced in Phase 2)
1405                let old_positional = shell_state.positional_params.clone();
1406
1407                // Set positional parameters for function arguments
1408                shell_state.set_positional_params(args.clone());
1409
1410                // Execute function body
1411                let exit_code = execute(function_body, shell_state);
1412
1413                // Check if we got an early return from the function
1414                if shell_state.is_returning() {
1415                    let return_value = shell_state.get_return_value().unwrap_or(0);
1416
1417                    // Restore old positional parameters
1418                    shell_state.set_positional_params(old_positional);
1419
1420                    // Exit function context
1421                    shell_state.exit_function();
1422
1423                    // Clear return state
1424                    shell_state.clear_return();
1425
1426                    // Update last_exit_code so $? captures the return value
1427                    shell_state.last_exit_code = return_value;
1428
1429                    // Return the early return value
1430                    return return_value;
1431                }
1432
1433                // Restore old positional parameters
1434                shell_state.set_positional_params(old_positional);
1435
1436                // Exit function context
1437                shell_state.exit_function();
1438
1439                // Update last_exit_code so $? captures the function's exit code
1440                shell_state.last_exit_code = exit_code;
1441
1442                exit_code
1443            } else {
1444                eprintln!("Function '{}' not found", name);
1445                1
1446            }
1447        }
1448        Ast::Return { value } => {
1449            // Return statements can only be used inside functions
1450            if shell_state.function_depth == 0 {
1451                eprintln!("Return statement outside of function");
1452                return 1;
1453            }
1454
1455            // Parse return value if provided
1456            let exit_code = if let Some(ref val) = value {
1457                val.parse::<i32>().unwrap_or(0)
1458            } else {
1459                0
1460            };
1461
1462            // Set return state to indicate early return from function
1463            shell_state.set_return(exit_code);
1464
1465            // Return the exit code - the function call handler will check for this
1466            exit_code
1467        }
1468        Ast::And { left, right } => {
1469            // Execute left side first
1470            let left_exit = execute(*left, shell_state);
1471
1472            // Check if we got an early return from a function
1473            if shell_state.is_returning() {
1474                return left_exit;
1475            }
1476
1477            // Only execute right side if left succeeded (exit code 0)
1478            if left_exit == 0 {
1479                execute(*right, shell_state)
1480            } else {
1481                left_exit
1482            }
1483        }
1484        Ast::Or { left, right } => {
1485            // Execute left side first
1486            let left_exit = execute(*left, shell_state);
1487
1488            // Check if we got an early return from a function
1489            if shell_state.is_returning() {
1490                return left_exit;
1491            }
1492
1493            // Only execute right side if left failed (exit code != 0)
1494            if left_exit != 0 {
1495                execute(*right, shell_state)
1496            } else {
1497                left_exit
1498            }
1499        }
1500        Ast::Subshell { body } => execute_subshell(*body, shell_state),
1501        Ast::CommandGroup { body } => execute(*body, shell_state),
1502    }
1503}
1504
1505fn execute_single_command(cmd: &ShellCommand, shell_state: &mut ShellState) -> i32 {
1506    // Check if this is a compound command (subshell)
1507    if let Some(ref compound_ast) = cmd.compound {
1508        // Execute compound command with redirections
1509        return execute_compound_with_redirections(compound_ast, shell_state, &cmd.redirections);
1510    }
1511
1512    if cmd.args.is_empty() {
1513        // No command, but may have redirections - process them for side effects
1514        if !cmd.redirections.is_empty() {
1515            if let Err(e) = apply_redirections(&cmd.redirections, shell_state, None) {
1516                if shell_state.colors_enabled {
1517                    eprintln!(
1518                        "{}Redirection error: {}\x1b[0m",
1519                        shell_state.color_scheme.error, e
1520                    );
1521                } else {
1522                    eprintln!("Redirection error: {}", e);
1523                }
1524                return 1;
1525            }
1526        }
1527        return 0;
1528    }
1529
1530    // First expand variables, then wildcards
1531    let var_expanded_args = expand_variables_in_args(&cmd.args, shell_state);
1532    let expanded_args = match expand_wildcards(&var_expanded_args) {
1533        Ok(args) => args,
1534        Err(_) => return 1,
1535    };
1536
1537    if expanded_args.is_empty() {
1538        return 0;
1539    }
1540
1541    // Check if this is a function call
1542    if shell_state.get_function(&expanded_args[0]).is_some() {
1543        // This is a function call - create a FunctionCall AST node and execute it
1544        let function_call = Ast::FunctionCall {
1545            name: expanded_args[0].clone(),
1546            args: expanded_args[1..].to_vec(),
1547        };
1548        return execute(function_call, shell_state);
1549    }
1550
1551    if crate::builtins::is_builtin(&expanded_args[0]) {
1552        // Create a temporary ShellCommand with expanded args
1553        let temp_cmd = ShellCommand {
1554            args: expanded_args,
1555            redirections: cmd.redirections.clone(),
1556            compound: None,
1557        };
1558
1559        // If we're capturing output, create a writer for it
1560        if let Some(ref capture_buffer) = shell_state.capture_output.clone() {
1561            // Create a writer that writes to our capture buffer
1562            struct CaptureWriter {
1563                buffer: Rc<RefCell<Vec<u8>>>,
1564            }
1565            impl std::io::Write for CaptureWriter {
1566                fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1567                    self.buffer.borrow_mut().extend_from_slice(buf);
1568                    Ok(buf.len())
1569                }
1570                fn flush(&mut self) -> std::io::Result<()> {
1571                    Ok(())
1572                }
1573            }
1574            let writer = CaptureWriter {
1575                buffer: capture_buffer.clone(),
1576            };
1577            crate::builtins::execute_builtin(&temp_cmd, shell_state, Some(Box::new(writer)))
1578        } else {
1579            crate::builtins::execute_builtin(&temp_cmd, shell_state, None)
1580        }
1581    } else {
1582        // Separate environment variable assignments from the actual command
1583        // Environment vars must come before the command and have the form VAR=value
1584        let mut env_assignments = Vec::new();
1585        let mut command_start_idx = 0;
1586
1587        for (idx, arg) in expanded_args.iter().enumerate() {
1588            // Check if this looks like an environment variable assignment
1589            if let Some(eq_pos) = arg.find('=')
1590                && eq_pos > 0
1591            {
1592                let var_part = &arg[..eq_pos];
1593                // Check if var_part is a valid variable name
1594                if var_part
1595                    .chars()
1596                    .next()
1597                    .map(|c| c.is_alphabetic() || c == '_')
1598                    .unwrap_or(false)
1599                    && var_part.chars().all(|c| c.is_alphanumeric() || c == '_')
1600                {
1601                    env_assignments.push(arg.clone());
1602                    command_start_idx = idx + 1;
1603                    continue;
1604                }
1605            }
1606            // If we reach here, this is not an env assignment, so we've found the command
1607            break;
1608        }
1609
1610        // Check if we have a command to execute (vs just env assignments)
1611        let has_command = command_start_idx < expanded_args.len();
1612
1613        // If all args were env assignments, set them in the shell
1614        // but continue to process redirections per POSIX
1615        if !has_command {
1616            for assignment in &env_assignments {
1617                if let Some(eq_pos) = assignment.find('=') {
1618                    let var_name = &assignment[..eq_pos];
1619                    let var_value = &assignment[eq_pos + 1..];
1620                    shell_state.set_var(var_name, var_value.to_string());
1621                }
1622            }
1623
1624            // Process redirections even without a command
1625            if !cmd.redirections.is_empty() {
1626                if let Err(e) = apply_redirections(&cmd.redirections, shell_state, None) {
1627                    if shell_state.colors_enabled {
1628                        eprintln!(
1629                            "{}Redirection error: {}\x1b[0m",
1630                            shell_state.color_scheme.error, e
1631                        );
1632                    } else {
1633                        eprintln!("Redirection error: {}", e);
1634                    }
1635                    return 1;
1636                }
1637            }
1638            return 0;
1639        }
1640
1641        // Prepare command
1642        let mut command = Command::new(&expanded_args[command_start_idx]);
1643        command.args(&expanded_args[command_start_idx + 1..]);
1644
1645        // Check for stdin override (for pipeline subshells)
1646        if let Some(fd) = shell_state.stdin_override {
1647            unsafe {
1648                let dup_fd = libc::dup(fd);
1649                if dup_fd >= 0 {
1650                    command.stdin(Stdio::from_raw_fd(dup_fd));
1651                }
1652            }
1653        }
1654
1655        // Set environment for child process
1656        let mut child_env = shell_state.get_env_for_child();
1657
1658        // Add the per-command environment variable assignments
1659        for assignment in env_assignments {
1660            if let Some(eq_pos) = assignment.find('=') {
1661                let var_name = assignment[..eq_pos].to_string();
1662                let var_value = assignment[eq_pos + 1..].to_string();
1663                child_env.insert(var_name, var_value);
1664            }
1665        }
1666
1667        command.env_clear();
1668        for (key, value) in child_env {
1669            command.env(key, value);
1670        }
1671
1672        // If we're capturing output, redirect stdout to capture buffer
1673        let capturing = shell_state.capture_output.is_some();
1674        if capturing {
1675            command.stdout(Stdio::piped());
1676        }
1677
1678        // Apply all redirections
1679        if let Err(e) = apply_redirections(&cmd.redirections, shell_state, Some(&mut command)) {
1680            if shell_state.colors_enabled {
1681                eprintln!(
1682                    "{}Redirection error: {}\x1b[0m",
1683                    shell_state.color_scheme.error, e
1684                );
1685            } else {
1686                eprintln!("Redirection error: {}", e);
1687            }
1688            return 1;
1689        }
1690
1691        // Apply custom file descriptors (3-9) from fd table to external command
1692        // We need to keep the FD table borrowed until after the child is spawned
1693        // to prevent File handles from being dropped and FDs from being closed
1694        let custom_fds: Vec<(i32, RawFd)> = {
1695            let fd_table = shell_state.fd_table.borrow();
1696            let mut fds = Vec::new();
1697
1698            for fd_num in 3..=9 {
1699                if fd_table.is_open(fd_num) {
1700                    if let Some(raw_fd) = fd_table.get_raw_fd(fd_num) {
1701                        fds.push((fd_num, raw_fd));
1702                    }
1703                }
1704            }
1705
1706            fds
1707        };
1708
1709        // If we have custom fds to apply, use pre_exec to set them in the child
1710        if !custom_fds.is_empty() {
1711            unsafe {
1712                command.pre_exec(move || {
1713                    for (target_fd, source_fd) in &custom_fds {
1714                        let result = libc::dup2(*source_fd, *target_fd);
1715                        if result < 0 {
1716                            return Err(std::io::Error::last_os_error());
1717                        }
1718                    }
1719                    Ok(())
1720                });
1721            }
1722        }
1723
1724        // Spawn and execute the command
1725        // Note: The FD table borrow above has been released, but the custom_fds
1726        // closure capture keeps the file handles alive
1727        match command.spawn() {
1728            Ok(mut child) => {
1729                // If capturing, read stdout
1730                if capturing {
1731                    if let Some(mut stdout) = child.stdout.take() {
1732                        use std::io::Read;
1733                        let mut output = Vec::new();
1734                        if stdout.read_to_end(&mut output).is_ok() {
1735                            if let Some(ref capture_buffer) = shell_state.capture_output {
1736                                capture_buffer.borrow_mut().extend_from_slice(&output);
1737                            }
1738                        }
1739                    }
1740                }
1741
1742                match child.wait() {
1743                    Ok(status) => status.code().unwrap_or(0),
1744                    Err(e) => {
1745                        if shell_state.colors_enabled {
1746                            eprintln!(
1747                                "{}Error waiting for command: {}\x1b[0m",
1748                                shell_state.color_scheme.error, e
1749                            );
1750                        } else {
1751                            eprintln!("Error waiting for command: {}", e);
1752                        }
1753                        1
1754                    }
1755                }
1756            }
1757            Err(e) => {
1758                if shell_state.colors_enabled {
1759                    eprintln!(
1760                        "{}Command spawn error: {}\x1b[0m",
1761                        shell_state.color_scheme.error, e
1762                    );
1763                } else {
1764                    eprintln!("Command spawn error: {}", e);
1765                }
1766                1
1767            }
1768        }
1769    }
1770}
1771
1772fn execute_pipeline(commands: &[ShellCommand], shell_state: &mut ShellState) -> i32 {
1773    let mut exit_code = 0;
1774    let mut previous_stdout: Option<File> = None;
1775
1776    for (i, cmd) in commands.iter().enumerate() {
1777        let is_last = i == commands.len() - 1;
1778
1779        if let Some(ref compound_ast) = cmd.compound {
1780            // Execute compound command (subshell) in pipeline
1781            let (com_exit_code, com_stdout) = execute_compound_in_pipeline(
1782                compound_ast,
1783                shell_state,
1784                previous_stdout.take(),
1785                i == 0,
1786                is_last,
1787                &cmd.redirections,
1788            );
1789            exit_code = com_exit_code;
1790            previous_stdout = com_stdout;
1791            continue;
1792        }
1793
1794        if cmd.args.is_empty() {
1795            continue;
1796        }
1797
1798        // First expand variables, then wildcards
1799        let var_expanded_args = expand_variables_in_args(&cmd.args, shell_state);
1800        let expanded_args = match expand_wildcards(&var_expanded_args) {
1801            Ok(args) => args,
1802            Err(_) => return 1,
1803        };
1804
1805        if expanded_args.is_empty() {
1806            continue;
1807        }
1808
1809        if crate::builtins::is_builtin(&expanded_args[0]) {
1810            // Built-ins in pipelines are tricky - for now, execute them separately
1811            // This is not perfect but better than nothing
1812            let temp_cmd = ShellCommand {
1813                args: expanded_args,
1814                redirections: cmd.redirections.clone(),
1815                compound: None,
1816            };
1817            if !is_last {
1818                // Create a safe pipe
1819                let (reader, writer) = match pipe() {
1820                    Ok((r, w)) => (unsafe { File::from_raw_fd(r.into_raw_fd()) }, w),
1821                    Err(e) => {
1822                        if shell_state.colors_enabled {
1823                            eprintln!(
1824                                "{}Error creating pipe for builtin: {}\x1b[0m",
1825                                shell_state.color_scheme.error, e
1826                            );
1827                        } else {
1828                            eprintln!("Error creating pipe for builtin: {}", e);
1829                        }
1830                        return 1;
1831                    }
1832                };
1833                // Execute builtin with writer for output capture
1834                exit_code = crate::builtins::execute_builtin(
1835                    &temp_cmd,
1836                    shell_state,
1837                    Some(Box::new(writer)),
1838                );
1839                // Use reader for next command's stdin
1840                previous_stdout = Some(reader);
1841            } else {
1842                // Last command: check if we're capturing output
1843                if let Some(ref capture_buffer) = shell_state.capture_output.clone() {
1844                    // Create a writer that writes to our capture buffer
1845                    struct CaptureWriter {
1846                        buffer: Rc<RefCell<Vec<u8>>>,
1847                    }
1848                    impl std::io::Write for CaptureWriter {
1849                        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1850                            self.buffer.borrow_mut().extend_from_slice(buf);
1851                            Ok(buf.len())
1852                        }
1853                        fn flush(&mut self) -> std::io::Result<()> {
1854                            Ok(())
1855                        }
1856                    }
1857                    let writer = CaptureWriter {
1858                        buffer: capture_buffer.clone(),
1859                    };
1860                    exit_code = crate::builtins::execute_builtin(
1861                        &temp_cmd,
1862                        shell_state,
1863                        Some(Box::new(writer)),
1864                    );
1865                } else {
1866                    // Not capturing, execute normally
1867                    exit_code = crate::builtins::execute_builtin(&temp_cmd, shell_state, None);
1868                }
1869                previous_stdout = None;
1870            }
1871        } else {
1872            let mut command = Command::new(&expanded_args[0]);
1873            command.args(&expanded_args[1..]);
1874
1875            // Set environment for child process
1876            let child_env = shell_state.get_env_for_child();
1877            command.env_clear();
1878            for (key, value) in child_env {
1879                command.env(key, value);
1880            }
1881
1882            // Set stdin from previous command's stdout
1883            if let Some(prev) = previous_stdout.take() {
1884                command.stdin(Stdio::from(prev));
1885            } else if i > 0 {
1886                // We are in a pipeline (not first command) but have no input pipe.
1887                // This means the previous command didn't produce a pipe.
1888                // We should treat this as empty input (EOF), not inherit stdin!
1889                command.stdin(Stdio::null());
1890            } else if let Some(fd) = shell_state.stdin_override {
1891                // We have a stdin override (e.g. from parent subshell)
1892                // We must duplicate it because Stdio takes ownership
1893                unsafe {
1894                    let dup_fd = libc::dup(fd);
1895                    if dup_fd >= 0 {
1896                        command.stdin(Stdio::from_raw_fd(dup_fd));
1897                    }
1898                }
1899            }
1900
1901            // Set stdout for next command, or for capturing if this is the last
1902            if !is_last {
1903                command.stdout(Stdio::piped());
1904            } else if shell_state.capture_output.is_some() {
1905                // Last command in pipeline but we're capturing output
1906                command.stdout(Stdio::piped());
1907            }
1908
1909            // Apply redirections for this command
1910            if let Err(e) = apply_redirections(&cmd.redirections, shell_state, Some(&mut command)) {
1911                if shell_state.colors_enabled {
1912                    eprintln!(
1913                        "{}Redirection error: {}\x1b[0m",
1914                        shell_state.color_scheme.error, e
1915                    );
1916                } else {
1917                    eprintln!("Redirection error: {}", e);
1918                }
1919                return 1;
1920            }
1921
1922            match command.spawn() {
1923                Ok(mut child) => {
1924                    if !is_last {
1925                        previous_stdout = child
1926                            .stdout
1927                            .take()
1928                            .map(|s| unsafe { File::from_raw_fd(s.into_raw_fd()) });
1929                    } else if shell_state.capture_output.is_some() {
1930                        // Last command and we're capturing - read its output
1931                        if let Some(mut stdout) = child.stdout.take() {
1932                            use std::io::Read;
1933                            let mut output = Vec::new();
1934                            if stdout.read_to_end(&mut output).is_ok()
1935                                && let Some(ref capture_buffer) = shell_state.capture_output
1936                            {
1937                                capture_buffer.borrow_mut().extend_from_slice(&output);
1938                            }
1939                        }
1940                    }
1941                    match child.wait() {
1942                        Ok(status) => {
1943                            exit_code = status.code().unwrap_or(0);
1944                        }
1945                        Err(e) => {
1946                            if shell_state.colors_enabled {
1947                                eprintln!(
1948                                    "{}Error waiting for command: {}\x1b[0m",
1949                                    shell_state.color_scheme.error, e
1950                                );
1951                            } else {
1952                                eprintln!("Error waiting for command: {}", e);
1953                            }
1954                            exit_code = 1;
1955                        }
1956                    }
1957                }
1958                Err(e) => {
1959                    if shell_state.colors_enabled {
1960                        eprintln!(
1961                            "{}Error spawning command '{}{}",
1962                            shell_state.color_scheme.error,
1963                            expanded_args[0],
1964                            &format!("': {}\x1b[0m", e)
1965                        );
1966                    } else {
1967                        eprintln!("Error spawning command '{}': {}", expanded_args[0], e);
1968                    }
1969                    exit_code = 1;
1970                }
1971            }
1972        }
1973    }
1974
1975    exit_code
1976}
1977
1978/// Execute a subshell with isolated state
1979///
1980/// # Arguments
1981/// * `body` - The AST to execute in the subshell
1982/// * `shell_state` - The parent shell state (will be cloned)
1983///
1984/// # Returns
1985/// * Exit code from the subshell execution
1986///
1987/// # Behavior
1988/// - Clones the shell state for isolation
1989/// - Executes the body in the cloned state
1990/// - Returns the exit code without modifying parent state
1991/// - Preserves parent state completely (variables, functions, etc.)
1992/// - Tracks subshell depth to prevent stack overflow
1993/// - Handles exit and return commands properly (isolated from parent)
1994/// - Cleans up file descriptors to prevent resource leaks
1995fn execute_subshell(body: Ast, shell_state: &mut ShellState) -> i32 {
1996    // Check depth limit to prevent stack overflow
1997    if shell_state.subshell_depth >= MAX_SUBSHELL_DEPTH {
1998        if shell_state.colors_enabled {
1999            eprintln!(
2000                "{}Subshell nesting limit ({}) exceeded\x1b[0m",
2001                shell_state.color_scheme.error, MAX_SUBSHELL_DEPTH
2002            );
2003        } else {
2004            eprintln!("Subshell nesting limit ({}) exceeded", MAX_SUBSHELL_DEPTH);
2005        }
2006        shell_state.last_exit_code = 1;
2007        return 1;
2008    }
2009
2010    // Save current directory for restoration
2011    let original_dir = std::env::current_dir().ok();
2012
2013    // Clone the shell state for isolation
2014    let mut subshell_state = shell_state.clone();
2015
2016    // Deep clone the file descriptor table for isolation
2017    // shell_state.clone() only clones the Rc, so we need to manually deep clone the table
2018    // and put it in a new Rc<RefCell<_>>
2019    match shell_state.fd_table.borrow().deep_clone() {
2020        Ok(new_fd_table) => {
2021            subshell_state.fd_table = Rc::new(RefCell::new(new_fd_table));
2022        }
2023        Err(e) => {
2024            if shell_state.colors_enabled {
2025                eprintln!(
2026                    "{}Failed to clone file descriptor table: {}\x1b[0m",
2027                    shell_state.color_scheme.error, e
2028                );
2029            } else {
2030                eprintln!("Failed to clone file descriptor table: {}", e);
2031            }
2032            return 1;
2033        }
2034    }
2035
2036    // Increment subshell depth in the cloned state
2037    subshell_state.subshell_depth = shell_state.subshell_depth + 1;
2038
2039    // Clone trap handlers for isolation (subshells inherit but don't affect parent)
2040    let parent_traps = shell_state.trap_handlers.lock().unwrap().clone();
2041    subshell_state.trap_handlers = std::sync::Arc::new(std::sync::Mutex::new(parent_traps));
2042
2043    // Execute the body in the isolated state
2044    let exit_code = execute(body, &mut subshell_state);
2045
2046    // Handle exit in subshell: exit should only exit the subshell, not the parent
2047    // The exit_requested flag is isolated to the subshell_state, so it won't affect parent
2048    let final_exit_code = if subshell_state.exit_requested {
2049        // Subshell called exit - use its exit code
2050        subshell_state.exit_code
2051    } else if subshell_state.is_returning() {
2052        // Subshell called return - treat as exit from subshell
2053        // Return in subshell should not propagate to parent function
2054        subshell_state.get_return_value().unwrap_or(exit_code)
2055    } else {
2056        exit_code
2057    };
2058
2059    // Clean up the subshell's file descriptor table to prevent resource leaks
2060    // This ensures any file descriptors opened in the subshell are properly released
2061    subshell_state.fd_table.borrow_mut().clear();
2062
2063    // Restore original directory (in case subshell changed it)
2064    if let Some(dir) = original_dir {
2065        let _ = std::env::set_current_dir(dir);
2066    }
2067
2068    // Update parent's last_exit_code to reflect subshell result
2069    shell_state.last_exit_code = final_exit_code;
2070
2071    // Return the exit code
2072    final_exit_code
2073}
2074
2075/// Execute a compound command with redirections
2076///
2077/// # Arguments
2078/// * `compound_ast` - The compound command AST
2079/// * `shell_state` - The shell state
2080/// * `redirections` - Redirections to apply
2081///
2082/// # Returns
2083/// * Exit code from the compound command
2084fn execute_compound_with_redirections(
2085    compound_ast: &Ast,
2086    shell_state: &mut ShellState,
2087    redirections: &[Redirection],
2088) -> i32 {
2089    match compound_ast {
2090        Ast::CommandGroup { body } => {
2091            // Save FDs before applying redirections
2092            if let Err(e) = shell_state.fd_table.borrow_mut().save_all_fds() {
2093                eprintln!("Error saving FDs: {}", e);
2094                return 1;
2095            }
2096
2097            // Apply redirections to current process
2098            if let Err(e) = apply_redirections(redirections, shell_state, None) {
2099                if shell_state.colors_enabled {
2100                    eprintln!("{}{}\u{001b}[0m", shell_state.color_scheme.error, e);
2101                } else {
2102                    eprintln!("{}", e);
2103                }
2104                shell_state.fd_table.borrow_mut().restore_all_fds().ok();
2105                return 1;
2106            }
2107
2108            // Execute the group body
2109            let exit_code = execute(*body.clone(), shell_state);
2110
2111            // Restore FDs
2112            if let Err(e) = shell_state.fd_table.borrow_mut().restore_all_fds() {
2113                eprintln!("Error restoring FDs: {}", e);
2114            }
2115
2116            exit_code
2117        }
2118        Ast::Subshell { body } => {
2119            // For subshells with redirections, we need to:
2120            // 1. Set up output capture if there are output redirections
2121            // 2. Execute the subshell
2122            // 3. Apply the redirections to the captured output
2123
2124            // Check if we have output redirections
2125            let has_output_redir = redirections.iter().any(|r| {
2126                matches!(
2127                    r,
2128                    Redirection::Output(_)
2129                        | Redirection::Append(_)
2130                        | Redirection::FdOutput(_, _)
2131                        | Redirection::FdAppend(_, _)
2132                )
2133            });
2134
2135            if has_output_redir {
2136                // Clone state for subshell
2137                let mut subshell_state = shell_state.clone();
2138
2139                // Set up output capture
2140                let capture_buffer = Rc::new(RefCell::new(Vec::new()));
2141                subshell_state.capture_output = Some(capture_buffer.clone());
2142
2143                // Execute subshell
2144                let exit_code = execute(*body.clone(), &mut subshell_state);
2145
2146                // Get captured output
2147                let output = capture_buffer.borrow().clone();
2148
2149                // Apply redirections to output
2150                for redir in redirections {
2151                    match redir {
2152                        Redirection::Output(file) => {
2153                            let expanded_file = expand_variables_in_string(file, shell_state);
2154                            if let Err(e) = std::fs::write(&expanded_file, &output) {
2155                                if shell_state.colors_enabled {
2156                                    eprintln!(
2157                                        "{}Redirection error: {}\x1b[0m",
2158                                        shell_state.color_scheme.error, e
2159                                    );
2160                                } else {
2161                                    eprintln!("Redirection error: {}", e);
2162                                }
2163                                return 1;
2164                            }
2165                        }
2166                        Redirection::Append(file) => {
2167                            let expanded_file = expand_variables_in_string(file, shell_state);
2168                            use std::fs::OpenOptions;
2169                            let mut file_handle = match OpenOptions::new()
2170                                .append(true)
2171                                .create(true)
2172                                .open(&expanded_file)
2173                            {
2174                                Ok(f) => f,
2175                                Err(e) => {
2176                                    if shell_state.colors_enabled {
2177                                        eprintln!(
2178                                            "{}Redirection error: {}\x1b[0m",
2179                                            shell_state.color_scheme.error, e
2180                                        );
2181                                    } else {
2182                                        eprintln!("Redirection error: {}", e);
2183                                    }
2184                                    return 1;
2185                                }
2186                            };
2187                            if let Err(e) = file_handle.write_all(&output) {
2188                                if shell_state.colors_enabled {
2189                                    eprintln!(
2190                                        "{}Redirection error: {}\x1b[0m",
2191                                        shell_state.color_scheme.error, e
2192                                    );
2193                                } else {
2194                                    eprintln!("Redirection error: {}", e);
2195                                }
2196                                return 1;
2197                            }
2198                        }
2199                        _ => {
2200                            // For Phase 2, only support basic output redirections
2201                            // Other redirections are silently ignored for subshells
2202                        }
2203                    }
2204                }
2205
2206                shell_state.last_exit_code = exit_code;
2207                exit_code
2208            } else {
2209                // No output redirections, execute normally
2210                execute_subshell(*body.clone(), shell_state)
2211            }
2212        }
2213        _ => {
2214            eprintln!("Unsupported compound command type");
2215            1
2216        }
2217    }
2218}
2219
2220/// Check if redirections include stdout redirections
2221/// Returns true if any redirection affects stdout (FD 1)
2222fn has_stdout_redirection(redirections: &[Redirection]) -> bool {
2223    redirections.iter().any(|r| match r {
2224        // Default output redirections affect stdout (FD 1)
2225        Redirection::Output(_) | Redirection::Append(_) => true,
2226        // Explicit FD 1 redirections
2227        Redirection::FdOutput(1, _) | Redirection::FdAppend(1, _) => true,
2228        // FD 1 duplication or closure
2229        Redirection::FdDuplicate(1, _) | Redirection::FdClose(1) => true,
2230        // All other redirections don't affect stdout
2231        _ => false,
2232    })
2233}
2234
2235/// Execute a compound command (subshell) as part of a pipeline
2236///
2237/// # Arguments
2238/// * `compound_ast` - The compound command AST (typically Subshell)
2239/// * `shell_state` - The parent shell state
2240/// * `is_last` - Whether this is the last command in the pipeline
2241/// * `redirections` - Redirections to apply to the compound command
2242///
2243/// # Returns
2244/// * Exit code from the compound command
2245fn execute_compound_in_pipeline(
2246    compound_ast: &Ast,
2247    shell_state: &mut ShellState,
2248    stdin: Option<File>,
2249    is_first: bool,
2250    is_last: bool,
2251    redirections: &[Redirection],
2252) -> (i32, Option<File>) {
2253    match compound_ast {
2254        Ast::Subshell { body } | Ast::CommandGroup { body } => {
2255            // Clone state for subshell
2256            let mut subshell_state = shell_state.clone();
2257
2258            // Setup stdin from provided file if available
2259            // We must keep the file alive for the duration of the subshell execution.
2260            let mut _stdin_file = stdin;
2261
2262            if let Some(ref f) = _stdin_file {
2263                let fd = f.as_raw_fd();
2264                subshell_state.stdin_override = Some(fd);
2265            } else if !is_first && subshell_state.stdin_override.is_none() {
2266                // If we have no input from previous stage and no override, use /dev/null
2267                if let Ok(f) = File::open("/dev/null") {
2268                    subshell_state.stdin_override = Some(f.as_raw_fd());
2269                    _stdin_file = Some(f);
2270                }
2271            }
2272
2273            // Setup output capture if not last or if parent is capturing
2274            // BUT skip capture if stdout is redirected (e.g., { pwd; } > out | wc -l)
2275            let capture_buffer = if (!is_last || shell_state.capture_output.is_some())
2276                && !has_stdout_redirection(redirections)
2277            {
2278                let buffer = Rc::new(RefCell::new(Vec::new()));
2279                subshell_state.capture_output = Some(buffer.clone());
2280                Some(buffer)
2281            } else {
2282                None
2283            };
2284
2285            // Apply redirections (saving/restoring if it's a group)
2286            let exit_code = if matches!(compound_ast, Ast::CommandGroup { .. }) {
2287                // Save FDs before applying redirections
2288                if let Err(e) = subshell_state.fd_table.borrow_mut().save_all_fds() {
2289                    eprintln!("Error saving FDs: {}", e);
2290                    return (1, None);
2291                }
2292
2293                // If we have a pipe from previous stage, hook it up to FD 0 for builtins
2294                if let Some(ref f) = _stdin_file {
2295                    unsafe {
2296                        libc::dup2(f.as_raw_fd(), 0);
2297                    }
2298                }
2299
2300                // Apply redirections to current process
2301                if let Err(e) = apply_redirections(redirections, &mut subshell_state, None) {
2302                    if subshell_state.colors_enabled {
2303                        eprintln!("{}{}\u{001b}[0m", subshell_state.color_scheme.error, e);
2304                    } else {
2305                        eprintln!("{}", e);
2306                    }
2307                    subshell_state.fd_table.borrow_mut().restore_all_fds().ok();
2308                    return (1, None);
2309                }
2310
2311                // Execute the body
2312                let code = execute(*body.clone(), &mut subshell_state);
2313
2314                // Restore FDs
2315                if let Err(e) = subshell_state.fd_table.borrow_mut().restore_all_fds() {
2316                    eprintln!("Error restoring FDs: {}", e);
2317                }
2318                code
2319            } else {
2320                // Subshell handling (non-forking)
2321                if let Err(e) = subshell_state.fd_table.borrow_mut().save_all_fds() {
2322                    eprintln!("Error saving FDs: {}", e);
2323                    return (1, None);
2324                }
2325
2326                // If we have a pipe from previous stage, hook it up to FD 0
2327                if let Some(ref f) = _stdin_file {
2328                    unsafe {
2329                        libc::dup2(f.as_raw_fd(), 0);
2330                    }
2331                }
2332
2333                if let Err(e) = apply_redirections(redirections, &mut subshell_state, None) {
2334                    eprintln!("{}", e);
2335                    subshell_state.fd_table.borrow_mut().restore_all_fds().ok();
2336                    return (1, None);
2337                }
2338                let code = execute(*body.clone(), &mut subshell_state);
2339                subshell_state.fd_table.borrow_mut().restore_all_fds().ok();
2340                code
2341            };
2342
2343            // Prepare stdout for next stage if captured
2344            let mut next_stdout = None;
2345            if let Some(buffer) = capture_buffer {
2346                let captured = buffer.borrow().clone();
2347
2348                // If not last, create a pipe and write captured output to it
2349                if !is_last {
2350                    use std::io::Write;
2351                    let (reader, mut writer) = match pipe() {
2352                        Ok((r, w)) => (r, w),
2353                        Err(e) => {
2354                            eprintln!("Error creating pipe for compound command: {}", e);
2355                            return (exit_code, None);
2356                        }
2357                    };
2358                    if let Err(e) = writer.write_all(&captured) {
2359                        eprintln!("Error writing to pipe: {}", e);
2360                    }
2361                    drop(writer); // Close write end so reader sees EOF
2362
2363                    next_stdout = Some(unsafe { File::from_raw_fd(reader.into_raw_fd()) });
2364                }
2365
2366                // If parent is capturing, also pass data up
2367                if let Some(ref parent_capture) = shell_state.capture_output {
2368                    parent_capture.borrow_mut().extend_from_slice(&captured);
2369                }
2370            }
2371
2372            shell_state.last_exit_code = exit_code;
2373            (exit_code, next_stdout)
2374        }
2375        _ => {
2376            eprintln!("Unsupported compound command in pipeline");
2377            (1, None)
2378        }
2379    }
2380}
2381
2382#[cfg(test)]
2383mod tests {
2384    use super::*;
2385    use std::sync::Mutex;
2386
2387    // Mutex to serialize tests that modify environment variables or create files
2388    static ENV_LOCK: Mutex<()> = Mutex::new(());
2389
2390    #[test]
2391    fn test_execute_single_command_builtin() {
2392        let cmd = ShellCommand {
2393            args: vec!["true".to_string()],
2394            redirections: Vec::new(),
2395            compound: None,
2396        };
2397        let mut shell_state = ShellState::new();
2398        let exit_code = execute_single_command(&cmd, &mut shell_state);
2399        assert_eq!(exit_code, 0);
2400    }
2401
2402    // For external commands, test with a command that exists
2403    #[test]
2404    fn test_execute_single_command_external() {
2405        let cmd = ShellCommand {
2406            args: vec!["true".to_string()], // Assume true exists
2407            redirections: Vec::new(),
2408            compound: None,
2409        };
2410        let mut shell_state = ShellState::new();
2411        let exit_code = execute_single_command(&cmd, &mut shell_state);
2412        assert_eq!(exit_code, 0);
2413    }
2414
2415    #[test]
2416    fn test_execute_single_command_external_nonexistent() {
2417        let cmd = ShellCommand {
2418            args: vec!["nonexistent_command".to_string()],
2419            redirections: Vec::new(),
2420            compound: None,
2421        };
2422        let mut shell_state = ShellState::new();
2423        let exit_code = execute_single_command(&cmd, &mut shell_state);
2424        assert_eq!(exit_code, 1); // Command not found
2425    }
2426
2427    #[test]
2428    fn test_execute_pipeline() {
2429        let commands = vec![
2430            ShellCommand {
2431                args: vec!["printf".to_string(), "hello".to_string()],
2432                redirections: Vec::new(),
2433                compound: None,
2434            },
2435            ShellCommand {
2436                args: vec!["cat".to_string()], // cat reads from stdin
2437                redirections: Vec::new(),
2438                compound: None,
2439            },
2440        ];
2441        let mut shell_state = ShellState::new();
2442        let exit_code = execute_pipeline(&commands, &mut shell_state);
2443        assert_eq!(exit_code, 0);
2444    }
2445
2446    #[test]
2447    fn test_execute_empty_pipeline() {
2448        let commands = vec![];
2449        let mut shell_state = ShellState::new();
2450        let exit_code = execute(Ast::Pipeline(commands), &mut shell_state);
2451        assert_eq!(exit_code, 0);
2452    }
2453
2454    #[test]
2455    fn test_execute_single_command() {
2456        let ast = Ast::Pipeline(vec![ShellCommand {
2457            args: vec!["true".to_string()],
2458            redirections: Vec::new(),
2459            compound: None,
2460        }]);
2461        let mut shell_state = ShellState::new();
2462        let exit_code = execute(ast, &mut shell_state);
2463        assert_eq!(exit_code, 0);
2464    }
2465
2466    #[test]
2467    fn test_execute_function_definition() {
2468        let ast = Ast::FunctionDefinition {
2469            name: "test_func".to_string(),
2470            body: Box::new(Ast::Pipeline(vec![ShellCommand {
2471                args: vec!["echo".to_string(), "hello".to_string()],
2472                redirections: Vec::new(),
2473                compound: None,
2474            }])),
2475        };
2476        let mut shell_state = ShellState::new();
2477        let exit_code = execute(ast, &mut shell_state);
2478        assert_eq!(exit_code, 0);
2479
2480        // Check that function was stored
2481        assert!(shell_state.get_function("test_func").is_some());
2482    }
2483
2484    #[test]
2485    fn test_execute_function_call() {
2486        // First define a function
2487        let mut shell_state = ShellState::new();
2488        shell_state.define_function(
2489            "test_func".to_string(),
2490            Ast::Pipeline(vec![ShellCommand {
2491                args: vec!["echo".to_string(), "hello".to_string()],
2492                redirections: Vec::new(),
2493                compound: None,
2494            }]),
2495        );
2496
2497        // Now call the function
2498        let ast = Ast::FunctionCall {
2499            name: "test_func".to_string(),
2500            args: vec![],
2501        };
2502        let exit_code = execute(ast, &mut shell_state);
2503        assert_eq!(exit_code, 0);
2504    }
2505
2506    #[test]
2507    fn test_execute_function_call_with_args() {
2508        // First define a function that uses arguments
2509        let mut shell_state = ShellState::new();
2510        shell_state.define_function(
2511            "test_func".to_string(),
2512            Ast::Pipeline(vec![ShellCommand {
2513                args: vec!["echo".to_string(), "arg1".to_string()],
2514                redirections: Vec::new(),
2515                compound: None,
2516            }]),
2517        );
2518
2519        // Now call the function with arguments
2520        let ast = Ast::FunctionCall {
2521            name: "test_func".to_string(),
2522            args: vec!["hello".to_string()],
2523        };
2524        let exit_code = execute(ast, &mut shell_state);
2525        assert_eq!(exit_code, 0);
2526    }
2527
2528    #[test]
2529    fn test_execute_nonexistent_function() {
2530        let mut shell_state = ShellState::new();
2531        let ast = Ast::FunctionCall {
2532            name: "nonexistent".to_string(),
2533            args: vec![],
2534        };
2535        let exit_code = execute(ast, &mut shell_state);
2536        assert_eq!(exit_code, 1); // Should return error code
2537    }
2538
2539    #[test]
2540    fn test_execute_function_integration() {
2541        // Test full integration: define function, then call it
2542        let mut shell_state = ShellState::new();
2543
2544        // First define a function
2545        let define_ast = Ast::FunctionDefinition {
2546            name: "hello".to_string(),
2547            body: Box::new(Ast::Pipeline(vec![ShellCommand {
2548                args: vec!["printf".to_string(), "Hello from function".to_string()],
2549                redirections: Vec::new(),
2550                compound: None,
2551            }])),
2552        };
2553        let exit_code = execute(define_ast, &mut shell_state);
2554        assert_eq!(exit_code, 0);
2555
2556        // Now call the function
2557        let call_ast = Ast::FunctionCall {
2558            name: "hello".to_string(),
2559            args: vec![],
2560        };
2561        let exit_code = execute(call_ast, &mut shell_state);
2562        assert_eq!(exit_code, 0);
2563    }
2564
2565    #[test]
2566    fn test_execute_function_with_local_variables() {
2567        let mut shell_state = ShellState::new();
2568
2569        // Set a global variable
2570        shell_state.set_var("global_var", "global_value".to_string());
2571
2572        // Define a function that uses local variables
2573        let define_ast = Ast::FunctionDefinition {
2574            name: "test_func".to_string(),
2575            body: Box::new(Ast::Sequence(vec![
2576                Ast::LocalAssignment {
2577                    var: "local_var".to_string(),
2578                    value: "local_value".to_string(),
2579                },
2580                Ast::Assignment {
2581                    var: "global_var".to_string(),
2582                    value: "modified_in_function".to_string(),
2583                },
2584                Ast::Pipeline(vec![ShellCommand {
2585                    args: vec!["printf".to_string(), "success".to_string()],
2586                    redirections: Vec::new(),
2587                    compound: None,
2588                }]),
2589            ])),
2590        };
2591        let exit_code = execute(define_ast, &mut shell_state);
2592        assert_eq!(exit_code, 0);
2593
2594        // Global variable should not be modified during function definition
2595        assert_eq!(
2596            shell_state.get_var("global_var"),
2597            Some("global_value".to_string())
2598        );
2599
2600        // Call the function
2601        let call_ast = Ast::FunctionCall {
2602            name: "test_func".to_string(),
2603            args: vec![],
2604        };
2605        let exit_code = execute(call_ast, &mut shell_state);
2606        assert_eq!(exit_code, 0);
2607
2608        // After function call, global variable should be modified since function assignments affect global scope
2609        assert_eq!(
2610            shell_state.get_var("global_var"),
2611            Some("modified_in_function".to_string())
2612        );
2613    }
2614
2615    #[test]
2616    fn test_execute_nested_function_calls() {
2617        let mut shell_state = ShellState::new();
2618
2619        // Set global variable
2620        shell_state.set_var("global_var", "global".to_string());
2621
2622        // Define outer function
2623        let outer_func = Ast::FunctionDefinition {
2624            name: "outer".to_string(),
2625            body: Box::new(Ast::Sequence(vec![
2626                Ast::Assignment {
2627                    var: "global_var".to_string(),
2628                    value: "outer_modified".to_string(),
2629                },
2630                Ast::FunctionCall {
2631                    name: "inner".to_string(),
2632                    args: vec![],
2633                },
2634                Ast::Pipeline(vec![ShellCommand {
2635                    args: vec!["printf".to_string(), "outer_done".to_string()],
2636                    redirections: Vec::new(),
2637                    compound: None,
2638                }]),
2639            ])),
2640        };
2641
2642        // Define inner function
2643        let inner_func = Ast::FunctionDefinition {
2644            name: "inner".to_string(),
2645            body: Box::new(Ast::Sequence(vec![
2646                Ast::Assignment {
2647                    var: "global_var".to_string(),
2648                    value: "inner_modified".to_string(),
2649                },
2650                Ast::Pipeline(vec![ShellCommand {
2651                    args: vec!["printf".to_string(), "inner_done".to_string()],
2652                    redirections: Vec::new(),
2653                    compound: None,
2654                }]),
2655            ])),
2656        };
2657
2658        // Define both functions
2659        execute(outer_func, &mut shell_state);
2660        execute(inner_func, &mut shell_state);
2661
2662        // Set initial global value
2663        shell_state.set_var("global_var", "initial".to_string());
2664
2665        // Call outer function (which calls inner function)
2666        let call_ast = Ast::FunctionCall {
2667            name: "outer".to_string(),
2668            args: vec![],
2669        };
2670        let exit_code = execute(call_ast, &mut shell_state);
2671        assert_eq!(exit_code, 0);
2672
2673        // After nested function calls, global variable should be modified by inner function
2674        // (bash behavior: function variable assignments affect global scope)
2675        assert_eq!(
2676            shell_state.get_var("global_var"),
2677            Some("inner_modified".to_string())
2678        );
2679    }
2680
2681    #[test]
2682    fn test_here_string_execution() {
2683        // Test here-string redirection with a simple command
2684        let cmd = ShellCommand {
2685            args: vec!["cat".to_string()],
2686            redirections: Vec::new(),
2687            compound: None,
2688            // TODO: Update test for new redirection system
2689        };
2690
2691        // Note: This test would require mocking stdin to provide the here-string content
2692        // For now, we'll just verify the command structure is parsed correctly
2693        assert_eq!(cmd.args, vec!["cat"]);
2694        // assert_eq!(cmd.here_string_content, Some("hello world".to_string()));
2695    }
2696
2697    #[test]
2698    fn test_here_document_execution() {
2699        // Test here-document redirection with a simple command
2700        let cmd = ShellCommand {
2701            args: vec!["cat".to_string()],
2702            redirections: Vec::new(),
2703            compound: None,
2704            // TODO: Update test for new redirection system
2705        };
2706
2707        // Note: This test would require mocking stdin to provide the here-document content
2708        // For now, we'll just verify the command structure is parsed correctly
2709        assert_eq!(cmd.args, vec!["cat"]);
2710        // assert_eq!(cmd.here_doc_delimiter, Some("EOF".to_string()));
2711    }
2712
2713    #[test]
2714    fn test_here_document_with_variable_expansion() {
2715        // Test that variables are expanded in here-document content
2716        let mut shell_state = ShellState::new();
2717        shell_state.set_var("PWD", "/test/path".to_string());
2718
2719        // Simulate here-doc content with variable
2720        let content = "Working dir: $PWD";
2721        let expanded = expand_variables_in_string(content, &mut shell_state);
2722
2723        assert_eq!(expanded, "Working dir: /test/path");
2724    }
2725
2726    #[test]
2727    fn test_here_document_with_command_substitution_builtin() {
2728        // Test that builtin command substitutions work in here-document content
2729        let mut shell_state = ShellState::new();
2730        shell_state.set_var("PWD", "/test/dir".to_string());
2731
2732        // Simulate here-doc content with pwd builtin command substitution
2733        let content = "Current directory: `pwd`";
2734        let expanded = expand_variables_in_string(content, &mut shell_state);
2735
2736        // The pwd builtin should be executed and expanded
2737        assert!(expanded.contains("Current directory: "));
2738    }
2739
2740    // ========================================================================
2741    // File Descriptor Integration Tests
2742    // ========================================================================
2743
2744    #[test]
2745    fn test_fd_output_redirection() {
2746        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2747
2748        // Create unique temp file
2749        use std::time::{SystemTime, UNIX_EPOCH};
2750        let timestamp = SystemTime::now()
2751            .duration_since(UNIX_EPOCH)
2752            .unwrap()
2753            .as_nanos();
2754        let temp_file = format!("/tmp/rush_test_fd_out_{}.txt", timestamp);
2755
2756        // Test: echo "error" 2>errors.txt
2757        let cmd = ShellCommand {
2758            args: vec![
2759                "sh".to_string(),
2760                "-c".to_string(),
2761                "echo error >&2".to_string(),
2762            ],
2763            redirections: vec![Redirection::FdOutput(2, temp_file.clone())],
2764            compound: None,
2765        };
2766
2767        let mut shell_state = ShellState::new();
2768        let exit_code = execute_single_command(&cmd, &mut shell_state);
2769        assert_eq!(exit_code, 0);
2770
2771        // Verify file was created and contains the error message
2772        let content = std::fs::read_to_string(&temp_file).unwrap();
2773        assert_eq!(content.trim(), "error");
2774
2775        // Cleanup
2776        let _ = std::fs::remove_file(&temp_file);
2777    }
2778
2779    #[test]
2780    fn test_fd_input_redirection() {
2781        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2782
2783        // Create unique temp file with content
2784        use std::time::{SystemTime, UNIX_EPOCH};
2785        let timestamp = SystemTime::now()
2786            .duration_since(UNIX_EPOCH)
2787            .unwrap()
2788            .as_nanos();
2789        let temp_file = format!("/tmp/rush_test_fd_in_{}.txt", timestamp);
2790
2791        std::fs::write(&temp_file, "test input\n").unwrap();
2792        std::thread::sleep(std::time::Duration::from_millis(10));
2793
2794        // Test: cat 3<input.txt (reading from fd 3)
2795        // Note: This tests that fd 3 is opened for reading
2796        let cmd = ShellCommand {
2797            args: vec!["cat".to_string()],
2798            compound: None,
2799            redirections: vec![
2800                Redirection::FdInput(3, temp_file.clone()),
2801                Redirection::Input(temp_file.clone()),
2802            ],
2803        };
2804
2805        let mut shell_state = ShellState::new();
2806        let exit_code = execute_single_command(&cmd, &mut shell_state);
2807        assert_eq!(exit_code, 0);
2808
2809        // Cleanup
2810        let _ = std::fs::remove_file(&temp_file);
2811    }
2812
2813    #[test]
2814    fn test_fd_append_redirection() {
2815        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2816
2817        // Create unique temp file with initial content
2818        use std::time::{SystemTime, UNIX_EPOCH};
2819        let timestamp = SystemTime::now()
2820            .duration_since(UNIX_EPOCH)
2821            .unwrap()
2822            .as_nanos();
2823        let temp_file = format!("/tmp/rush_test_fd_append_{}.txt", timestamp);
2824
2825        std::fs::write(&temp_file, "first line\n").unwrap();
2826        std::thread::sleep(std::time::Duration::from_millis(10));
2827
2828        // Test: echo "more" 2>>errors.txt
2829        let cmd = ShellCommand {
2830            args: vec![
2831                "sh".to_string(),
2832                "-c".to_string(),
2833                "echo second line >&2".to_string(),
2834            ],
2835            redirections: vec![Redirection::FdAppend(2, temp_file.clone())],
2836            compound: None,
2837        };
2838
2839        let mut shell_state = ShellState::new();
2840        let exit_code = execute_single_command(&cmd, &mut shell_state);
2841        assert_eq!(exit_code, 0);
2842
2843        // Verify file contains both lines
2844        let content = std::fs::read_to_string(&temp_file).unwrap();
2845        assert!(content.contains("first line"));
2846        assert!(content.contains("second line"));
2847
2848        // Cleanup
2849        let _ = std::fs::remove_file(&temp_file);
2850    }
2851
2852    #[test]
2853    fn test_fd_duplication_stderr_to_stdout() {
2854        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2855
2856        // Create unique temp file
2857        use std::time::{SystemTime, UNIX_EPOCH};
2858        let timestamp = SystemTime::now()
2859            .duration_since(UNIX_EPOCH)
2860            .unwrap()
2861            .as_nanos();
2862        let temp_file = format!("/tmp/rush_test_fd_dup_{}.txt", timestamp);
2863
2864        // Test: command 2>&1 >output.txt
2865        // Note: For external commands, fd duplication is handled by the shell
2866        // We test that the command executes successfully with the redirection
2867        let cmd = ShellCommand {
2868            args: vec![
2869                "sh".to_string(),
2870                "-c".to_string(),
2871                "echo test; echo error >&2".to_string(),
2872            ],
2873            compound: None,
2874            redirections: vec![Redirection::Output(temp_file.clone())],
2875        };
2876
2877        let mut shell_state = ShellState::new();
2878        let exit_code = execute_single_command(&cmd, &mut shell_state);
2879        assert_eq!(exit_code, 0);
2880
2881        // Verify file was created and contains output
2882        assert!(std::path::Path::new(&temp_file).exists());
2883        let content = std::fs::read_to_string(&temp_file).unwrap();
2884        assert!(content.contains("test"));
2885
2886        // Cleanup
2887        let _ = std::fs::remove_file(&temp_file);
2888    }
2889
2890    #[test]
2891    fn test_fd_close() {
2892        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2893
2894        // Test: command 2>&- (closes stderr)
2895        let cmd = ShellCommand {
2896            args: vec!["sh".to_string(), "-c".to_string(), "echo test".to_string()],
2897            redirections: vec![Redirection::FdClose(2)],
2898            compound: None,
2899        };
2900
2901        let mut shell_state = ShellState::new();
2902        let exit_code = execute_single_command(&cmd, &mut shell_state);
2903        assert_eq!(exit_code, 0);
2904
2905        // Verify fd 2 is closed in the fd table
2906        assert!(shell_state.fd_table.borrow().is_closed(2));
2907    }
2908
2909    #[test]
2910    fn test_fd_read_write() {
2911        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2912
2913        // Create unique temp file
2914        use std::time::{SystemTime, UNIX_EPOCH};
2915        let timestamp = SystemTime::now()
2916            .duration_since(UNIX_EPOCH)
2917            .unwrap()
2918            .as_nanos();
2919        let temp_file = format!("/tmp/rush_test_fd_rw_{}.txt", timestamp);
2920
2921        std::fs::write(&temp_file, "initial content\n").unwrap();
2922        std::thread::sleep(std::time::Duration::from_millis(10));
2923
2924        // Test: 3<>file.txt (opens fd 3 for read/write)
2925        let cmd = ShellCommand {
2926            args: vec!["cat".to_string()],
2927            compound: None,
2928            redirections: vec![
2929                Redirection::FdInputOutput(3, temp_file.clone()),
2930                Redirection::Input(temp_file.clone()),
2931            ],
2932        };
2933
2934        let mut shell_state = ShellState::new();
2935        let exit_code = execute_single_command(&cmd, &mut shell_state);
2936        assert_eq!(exit_code, 0);
2937
2938        // Cleanup
2939        let _ = std::fs::remove_file(&temp_file);
2940    }
2941
2942    #[test]
2943    fn test_multiple_fd_redirections() {
2944        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2945
2946        // Create unique temp files
2947        use std::time::{SystemTime, UNIX_EPOCH};
2948        let timestamp = SystemTime::now()
2949            .duration_since(UNIX_EPOCH)
2950            .unwrap()
2951            .as_nanos();
2952        let out_file = format!("/tmp/rush_test_fd_multi_out_{}.txt", timestamp);
2953        let err_file = format!("/tmp/rush_test_fd_multi_err_{}.txt", timestamp);
2954
2955        // Test: command 2>err.txt 1>out.txt
2956        let cmd = ShellCommand {
2957            args: vec![
2958                "sh".to_string(),
2959                "-c".to_string(),
2960                "echo stdout; echo stderr >&2".to_string(),
2961            ],
2962            redirections: vec![
2963                Redirection::FdOutput(2, err_file.clone()),
2964                Redirection::Output(out_file.clone()),
2965            ],
2966            compound: None,
2967        };
2968
2969        let mut shell_state = ShellState::new();
2970        let exit_code = execute_single_command(&cmd, &mut shell_state);
2971        assert_eq!(exit_code, 0);
2972
2973        // Verify both files were created
2974        assert!(std::path::Path::new(&out_file).exists());
2975        assert!(std::path::Path::new(&err_file).exists());
2976
2977        // Verify content
2978        let out_content = std::fs::read_to_string(&out_file).unwrap();
2979        let err_content = std::fs::read_to_string(&err_file).unwrap();
2980        assert!(out_content.contains("stdout"));
2981        assert!(err_content.contains("stderr"));
2982
2983        // Cleanup
2984        let _ = std::fs::remove_file(&out_file);
2985        let _ = std::fs::remove_file(&err_file);
2986    }
2987
2988    #[test]
2989    fn test_fd_swap_pattern() {
2990        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2991
2992        // Create unique temp files
2993        use std::time::{SystemTime, UNIX_EPOCH};
2994        let timestamp = SystemTime::now()
2995            .duration_since(UNIX_EPOCH)
2996            .unwrap()
2997            .as_nanos();
2998        let temp_file = format!("/tmp/rush_test_fd_swap_{}.txt", timestamp);
2999
3000        // Test fd operations: open fd 3, then close it
3001        // This tests the fd table operations
3002        let cmd = ShellCommand {
3003            args: vec!["sh".to_string(), "-c".to_string(), "echo test".to_string()],
3004            redirections: vec![
3005                Redirection::FdOutput(3, temp_file.clone()), // Open fd 3 for writing
3006                Redirection::FdClose(3),                     // Close fd 3
3007                Redirection::Output(temp_file.clone()),      // Write to stdout
3008            ],
3009            compound: None,
3010        };
3011
3012        let mut shell_state = ShellState::new();
3013        let exit_code = execute_single_command(&cmd, &mut shell_state);
3014        assert_eq!(exit_code, 0);
3015
3016        // Verify fd 3 is closed after the operations
3017        assert!(shell_state.fd_table.borrow().is_closed(3));
3018
3019        // Cleanup
3020        let _ = std::fs::remove_file(&temp_file);
3021    }
3022
3023    #[test]
3024    fn test_fd_redirection_with_pipes() {
3025        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3026
3027        // Create unique temp file
3028        use std::time::{SystemTime, UNIX_EPOCH};
3029        let timestamp = SystemTime::now()
3030            .duration_since(UNIX_EPOCH)
3031            .unwrap()
3032            .as_nanos();
3033        let temp_file = format!("/tmp/rush_test_fd_pipe_{}.txt", timestamp);
3034
3035        // Test: cmd1 | cmd2 >output.txt
3036        // This tests redirections in pipelines
3037        let commands = vec![
3038            ShellCommand {
3039                args: vec!["echo".to_string(), "piped output".to_string()],
3040                redirections: vec![],
3041                compound: None,
3042            },
3043            ShellCommand {
3044                args: vec!["cat".to_string()],
3045                compound: None,
3046                redirections: vec![Redirection::Output(temp_file.clone())],
3047            },
3048        ];
3049
3050        let mut shell_state = ShellState::new();
3051        let exit_code = execute_pipeline(&commands, &mut shell_state);
3052        assert_eq!(exit_code, 0);
3053
3054        // Verify output file contains the piped content
3055        let content = std::fs::read_to_string(&temp_file).unwrap();
3056        assert!(content.contains("piped output"));
3057
3058        // Cleanup
3059        let _ = std::fs::remove_file(&temp_file);
3060    }
3061
3062    #[test]
3063    fn test_fd_error_invalid_fd_number() {
3064        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3065
3066        // Create unique temp file
3067        use std::time::{SystemTime, UNIX_EPOCH};
3068        let timestamp = SystemTime::now()
3069            .duration_since(UNIX_EPOCH)
3070            .unwrap()
3071            .as_nanos();
3072        let temp_file = format!("/tmp/rush_test_fd_invalid_{}.txt", timestamp);
3073
3074        // Test: Invalid fd number (>1024)
3075        let cmd = ShellCommand {
3076            args: vec!["echo".to_string(), "test".to_string()],
3077            compound: None,
3078            redirections: vec![Redirection::FdOutput(1025, temp_file.clone())],
3079        };
3080
3081        let mut shell_state = ShellState::new();
3082        let exit_code = execute_single_command(&cmd, &mut shell_state);
3083
3084        // Should fail with error
3085        assert_eq!(exit_code, 1);
3086
3087        // Cleanup (file may not exist)
3088        let _ = std::fs::remove_file(&temp_file);
3089    }
3090
3091    #[test]
3092    fn test_fd_error_duplicate_closed_fd() {
3093        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3094
3095        // Test: Attempting to duplicate a closed fd
3096        let cmd = ShellCommand {
3097            args: vec!["echo".to_string(), "test".to_string()],
3098            compound: None,
3099            redirections: vec![
3100                Redirection::FdClose(3),
3101                Redirection::FdDuplicate(2, 3), // Try to duplicate closed fd 3
3102            ],
3103        };
3104
3105        let mut shell_state = ShellState::new();
3106        let exit_code = execute_single_command(&cmd, &mut shell_state);
3107
3108        // Should fail with error
3109        assert_eq!(exit_code, 1);
3110    }
3111
3112    #[test]
3113    fn test_fd_error_file_permission() {
3114        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3115
3116        // Test: Attempting to write to a read-only location
3117        let cmd = ShellCommand {
3118            args: vec!["echo".to_string(), "test".to_string()],
3119            redirections: vec![Redirection::FdOutput(2, "/proc/version".to_string())],
3120            compound: None,
3121        };
3122
3123        let mut shell_state = ShellState::new();
3124        let exit_code = execute_single_command(&cmd, &mut shell_state);
3125
3126        // Should fail with permission error
3127        assert_eq!(exit_code, 1);
3128    }
3129
3130    #[test]
3131    fn test_fd_redirection_order() {
3132        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3133
3134        // Create unique temp files
3135        use std::time::{SystemTime, UNIX_EPOCH};
3136        let timestamp = SystemTime::now()
3137            .duration_since(UNIX_EPOCH)
3138            .unwrap()
3139            .as_nanos();
3140        let file1 = format!("/tmp/rush_test_fd_order1_{}.txt", timestamp);
3141        let file2 = format!("/tmp/rush_test_fd_order2_{}.txt", timestamp);
3142
3143        // Test: Redirections are processed left-to-right
3144        // 1>file1 1>file2 should write to file2
3145        let cmd = ShellCommand {
3146            args: vec!["echo".to_string(), "test".to_string()],
3147            compound: None,
3148            redirections: vec![
3149                Redirection::Output(file1.clone()),
3150                Redirection::Output(file2.clone()),
3151            ],
3152        };
3153
3154        let mut shell_state = ShellState::new();
3155        let exit_code = execute_single_command(&cmd, &mut shell_state);
3156        assert_eq!(exit_code, 0);
3157
3158        // file2 should have the output (last redirection wins)
3159        let content2 = std::fs::read_to_string(&file2).unwrap();
3160        assert!(content2.contains("test"));
3161
3162        // Cleanup
3163        let _ = std::fs::remove_file(&file1);
3164        let _ = std::fs::remove_file(&file2);
3165    }
3166
3167    #[test]
3168    fn test_fd_builtin_with_redirection() {
3169        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3170
3171        // Create unique temp file
3172        use std::time::{SystemTime, UNIX_EPOCH};
3173        let timestamp = SystemTime::now()
3174            .duration_since(UNIX_EPOCH)
3175            .unwrap()
3176            .as_nanos();
3177        let temp_file = format!("/tmp/rush_test_fd_builtin_{}.txt", timestamp);
3178
3179        // Test: Built-in command with fd redirection
3180        let cmd = ShellCommand {
3181            args: vec!["echo".to_string(), "builtin test".to_string()],
3182            redirections: vec![Redirection::Output(temp_file.clone())],
3183            compound: None,
3184        };
3185
3186        let mut shell_state = ShellState::new();
3187        let exit_code = execute_single_command(&cmd, &mut shell_state);
3188        assert_eq!(exit_code, 0);
3189
3190        // Verify output
3191        let content = std::fs::read_to_string(&temp_file).unwrap();
3192        assert!(content.contains("builtin test"));
3193
3194        // Cleanup
3195        let _ = std::fs::remove_file(&temp_file);
3196    }
3197
3198    #[test]
3199    fn test_fd_variable_expansion_in_filename() {
3200        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3201
3202        // Create unique temp file
3203        use std::time::{SystemTime, UNIX_EPOCH};
3204        let timestamp = SystemTime::now()
3205            .duration_since(UNIX_EPOCH)
3206            .unwrap()
3207            .as_nanos();
3208        let temp_file = format!("/tmp/rush_test_fd_var_{}.txt", timestamp);
3209
3210        // Set variable for filename
3211        let mut shell_state = ShellState::new();
3212        shell_state.set_var("OUTFILE", temp_file.clone());
3213
3214        // Test: Variable expansion in redirection filename
3215        let cmd = ShellCommand {
3216            args: vec!["echo".to_string(), "variable test".to_string()],
3217            compound: None,
3218            redirections: vec![Redirection::Output("$OUTFILE".to_string())],
3219        };
3220
3221        let exit_code = execute_single_command(&cmd, &mut shell_state);
3222        assert_eq!(exit_code, 0);
3223
3224        // Verify output
3225        let content = std::fs::read_to_string(&temp_file).unwrap();
3226        assert!(content.contains("variable test"));
3227
3228        // Cleanup
3229        let _ = std::fs::remove_file(&temp_file);
3230    }
3231
3232    // ========================================================================
3233    // Break and Continue Integration Tests
3234    // ========================================================================
3235
3236    #[test]
3237    fn test_break_in_for_loop() {
3238        let mut shell_state = ShellState::new();
3239        shell_state.set_var("output", "".to_string());
3240        
3241        // for i in 1 2 3 4 5; do
3242        //   output="$output$i"
3243        //   if [ $i = "3" ]; then break; fi
3244        // done
3245        let ast = Ast::For {
3246            variable: "i".to_string(),
3247            items: vec!["1".to_string(), "2".to_string(), "3".to_string(), "4".to_string(), "5".to_string()],
3248            body: Box::new(Ast::Sequence(vec![
3249                Ast::Assignment {
3250                    var: "output".to_string(),
3251                    value: "$output$i".to_string(),
3252                },
3253                Ast::If {
3254                    branches: vec![(
3255                        Box::new(Ast::Pipeline(vec![ShellCommand {
3256                            args: vec!["test".to_string(), "$i".to_string(), "=".to_string(), "3".to_string()],
3257                            redirections: vec![],
3258                            compound: None,
3259                        }])),
3260                        Box::new(Ast::Pipeline(vec![ShellCommand {
3261                            args: vec!["break".to_string()],
3262                            redirections: vec![],
3263                            compound: None,
3264                        }])),
3265                    )],
3266                    else_branch: None,
3267                },
3268            ])),
3269        };
3270        
3271        let exit_code = execute(ast, &mut shell_state);
3272        assert_eq!(exit_code, 0);
3273        assert_eq!(shell_state.get_var("output"), Some("123".to_string()));
3274    }
3275
3276    #[test]
3277    fn test_continue_in_for_loop() {
3278        let mut shell_state = ShellState::new();
3279        shell_state.set_var("output", "".to_string());
3280        
3281        // for i in 1 2 3 4 5; do
3282        //   if [ $i = "3" ]; then continue; fi
3283        //   output="$output$i"
3284        // done
3285        let ast = Ast::For {
3286            variable: "i".to_string(),
3287            items: vec!["1".to_string(), "2".to_string(), "3".to_string(), "4".to_string(), "5".to_string()],
3288            body: Box::new(Ast::Sequence(vec![
3289                Ast::If {
3290                    branches: vec![(
3291                        Box::new(Ast::Pipeline(vec![ShellCommand {
3292                            args: vec!["test".to_string(), "$i".to_string(), "=".to_string(), "3".to_string()],
3293                            redirections: vec![],
3294                            compound: None,
3295                        }])),
3296                        Box::new(Ast::Pipeline(vec![ShellCommand {
3297                            args: vec!["continue".to_string()],
3298                            redirections: vec![],
3299                            compound: None,
3300                        }])),
3301                    )],
3302                    else_branch: None,
3303                },
3304                Ast::Assignment {
3305                    var: "output".to_string(),
3306                    value: "$output$i".to_string(),
3307                },
3308            ])),
3309        };
3310        
3311        let exit_code = execute(ast, &mut shell_state);
3312        assert_eq!(exit_code, 0);
3313        assert_eq!(shell_state.get_var("output"), Some("1245".to_string()));
3314    }
3315
3316    #[test]
3317    fn test_break_in_while_loop() {
3318        let mut shell_state = ShellState::new();
3319        shell_state.set_var("i", "0".to_string());
3320        shell_state.set_var("output", "".to_string());
3321        
3322        // i=0
3323        // while [ $i -lt 10 ]; do
3324        //   i=$((i + 1))
3325        //   output="$output$i"
3326        //   if [ $i = "5" ]; then break; fi
3327        // done
3328        let ast = Ast::While {
3329            condition: Box::new(Ast::Pipeline(vec![ShellCommand {
3330                args: vec!["test".to_string(), "$i".to_string(), "-lt".to_string(), "10".to_string()],
3331                redirections: vec![],
3332                compound: None,
3333            }])),
3334            body: Box::new(Ast::Sequence(vec![
3335                Ast::Assignment {
3336                    var: "i".to_string(),
3337                    value: "$((i + 1))".to_string(),
3338                },
3339                Ast::Assignment {
3340                    var: "output".to_string(),
3341                    value: "$output$i".to_string(),
3342                },
3343                Ast::If {
3344                    branches: vec![(
3345                        Box::new(Ast::Pipeline(vec![ShellCommand {
3346                            args: vec!["test".to_string(), "$i".to_string(), "=".to_string(), "5".to_string()],
3347                            redirections: vec![],
3348                            compound: None,
3349                        }])),
3350                        Box::new(Ast::Pipeline(vec![ShellCommand {
3351                            args: vec!["break".to_string()],
3352                            redirections: vec![],
3353                            compound: None,
3354                        }])),
3355                    )],
3356                    else_branch: None,
3357                },
3358            ])),
3359        };
3360        
3361        let exit_code = execute(ast, &mut shell_state);
3362        assert_eq!(exit_code, 0);
3363        assert_eq!(shell_state.get_var("output"), Some("12345".to_string()));
3364    }
3365
3366    #[test]
3367    fn test_continue_in_while_loop() {
3368        let mut shell_state = ShellState::new();
3369        shell_state.set_var("i", "0".to_string());
3370        shell_state.set_var("output", "".to_string());
3371        
3372        // i=0
3373        // while [ $i -lt 5 ]; do
3374        //   i=$((i + 1))
3375        //   if [ $i = "3" ]; then continue; fi
3376        //   output="$output$i"
3377        // done
3378        let ast = Ast::While {
3379            condition: Box::new(Ast::Pipeline(vec![ShellCommand {
3380                args: vec!["test".to_string(), "$i".to_string(), "-lt".to_string(), "5".to_string()],
3381                redirections: vec![],
3382                compound: None,
3383            }])),
3384            body: Box::new(Ast::Sequence(vec![
3385                Ast::Assignment {
3386                    var: "i".to_string(),
3387                    value: "$((i + 1))".to_string(),
3388                },
3389                Ast::If {
3390                    branches: vec![(
3391                        Box::new(Ast::Pipeline(vec![ShellCommand {
3392                            args: vec!["test".to_string(), "$i".to_string(), "=".to_string(), "3".to_string()],
3393                            redirections: vec![],
3394                            compound: None,
3395                        }])),
3396                        Box::new(Ast::Pipeline(vec![ShellCommand {
3397                            args: vec!["continue".to_string()],
3398                            redirections: vec![],
3399                            compound: None,
3400                        }])),
3401                    )],
3402                    else_branch: None,
3403                },
3404                Ast::Assignment {
3405                    var: "output".to_string(),
3406                    value: "$output$i".to_string(),
3407                },
3408            ])),
3409        };
3410        
3411        let exit_code = execute(ast, &mut shell_state);
3412        assert_eq!(exit_code, 0);
3413        assert_eq!(shell_state.get_var("output"), Some("1245".to_string()));
3414    }
3415
3416    #[test]
3417    fn test_break_nested_loops() {
3418        let mut shell_state = ShellState::new();
3419        shell_state.set_var("output", "".to_string());
3420        
3421        // for i in 1 2 3; do
3422        //   for j in a b c; do
3423        //     output="$output$i$j"
3424        //     if [ $j = "b" ]; then break; fi
3425        //   done
3426        // done
3427        let inner_loop = Ast::For {
3428            variable: "j".to_string(),
3429            items: vec!["a".to_string(), "b".to_string(), "c".to_string()],
3430            body: Box::new(Ast::Sequence(vec![
3431                Ast::Assignment {
3432                    var: "output".to_string(),
3433                    value: "$output$i$j".to_string(),
3434                },
3435                Ast::If {
3436                    branches: vec![(
3437                        Box::new(Ast::Pipeline(vec![ShellCommand {
3438                            args: vec!["test".to_string(), "$j".to_string(), "=".to_string(), "b".to_string()],
3439                            redirections: vec![],
3440                            compound: None,
3441                        }])),
3442                        Box::new(Ast::Pipeline(vec![ShellCommand {
3443                            args: vec!["break".to_string()],
3444                            redirections: vec![],
3445                            compound: None,
3446                        }])),
3447                    )],
3448                    else_branch: None,
3449                },
3450            ])),
3451        };
3452        
3453        let outer_loop = Ast::For {
3454            variable: "i".to_string(),
3455            items: vec!["1".to_string(), "2".to_string(), "3".to_string()],
3456            body: Box::new(inner_loop),
3457        };
3458        
3459        let exit_code = execute(outer_loop, &mut shell_state);
3460        assert_eq!(exit_code, 0);
3461        assert_eq!(shell_state.get_var("output"), Some("1a1b2a2b3a3b".to_string()));
3462    }
3463
3464    #[test]
3465    fn test_break_2_nested_loops() {
3466        let mut shell_state = ShellState::new();
3467        shell_state.set_var("output", "".to_string());
3468        
3469        // for i in 1 2 3; do
3470        //   for j in a b c; do
3471        //     output="$output$i$j"
3472        //     if [ $i = "2" ] && [ $j = "b" ]; then break 2; fi
3473        //   done
3474        // done
3475        let inner_loop = Ast::For {
3476            variable: "j".to_string(),
3477            items: vec!["a".to_string(), "b".to_string(), "c".to_string()],
3478            body: Box::new(Ast::Sequence(vec![
3479                Ast::Assignment {
3480                    var: "output".to_string(),
3481                    value: "$output$i$j".to_string(),
3482                },
3483                Ast::And {
3484                    left: Box::new(Ast::Pipeline(vec![ShellCommand {
3485                        args: vec!["test".to_string(), "$i".to_string(), "=".to_string(), "2".to_string()],
3486                        redirections: vec![],
3487                        compound: None,
3488                    }])),
3489                    right: Box::new(Ast::If {
3490                        branches: vec![(
3491                            Box::new(Ast::Pipeline(vec![ShellCommand {
3492                                args: vec!["test".to_string(), "$j".to_string(), "=".to_string(), "b".to_string()],
3493                                redirections: vec![],
3494                                compound: None,
3495                            }])),
3496                            Box::new(Ast::Pipeline(vec![ShellCommand {
3497                                args: vec!["break".to_string(), "2".to_string()],
3498                                redirections: vec![],
3499                                compound: None,
3500                            }])),
3501                        )],
3502                        else_branch: None,
3503                    }),
3504                },
3505            ])),
3506        };
3507        
3508        let outer_loop = Ast::For {
3509            variable: "i".to_string(),
3510            items: vec!["1".to_string(), "2".to_string(), "3".to_string()],
3511            body: Box::new(inner_loop),
3512        };
3513        
3514        let exit_code = execute(outer_loop, &mut shell_state);
3515        assert_eq!(exit_code, 0);
3516        assert_eq!(shell_state.get_var("output"), Some("1a1b1c2a2b".to_string()));
3517    }
3518
3519    #[test]
3520    fn test_continue_nested_loops() {
3521        let mut shell_state = ShellState::new();
3522        shell_state.set_var("output", "".to_string());
3523        
3524        // for i in 1 2 3; do
3525        //   for j in a b c; do
3526        //     if [ $j = "b" ]; then continue; fi
3527        //     output="$output$i$j"
3528        //   done
3529        // done
3530        let inner_loop = Ast::For {
3531            variable: "j".to_string(),
3532            items: vec!["a".to_string(), "b".to_string(), "c".to_string()],
3533            body: Box::new(Ast::Sequence(vec![
3534                Ast::If {
3535                    branches: vec![(
3536                        Box::new(Ast::Pipeline(vec![ShellCommand {
3537                            args: vec!["test".to_string(), "$j".to_string(), "=".to_string(), "b".to_string()],
3538                            redirections: vec![],
3539                            compound: None,
3540                        }])),
3541                        Box::new(Ast::Pipeline(vec![ShellCommand {
3542                            args: vec!["continue".to_string()],
3543                            redirections: vec![],
3544                            compound: None,
3545                        }])),
3546                    )],
3547                    else_branch: None,
3548                },
3549                Ast::Assignment {
3550                    var: "output".to_string(),
3551                    value: "$output$i$j".to_string(),
3552                },
3553            ])),
3554        };
3555        
3556        let outer_loop = Ast::For {
3557            variable: "i".to_string(),
3558            items: vec!["1".to_string(), "2".to_string(), "3".to_string()],
3559            body: Box::new(inner_loop),
3560        };
3561        
3562        let exit_code = execute(outer_loop, &mut shell_state);
3563        assert_eq!(exit_code, 0);
3564        assert_eq!(shell_state.get_var("output"), Some("1a1c2a2c3a3c".to_string()));
3565    }
3566
3567    #[test]
3568    fn test_continue_2_nested_loops() {
3569        let mut shell_state = ShellState::new();
3570        shell_state.set_var("output", "".to_string());
3571        
3572        // for i in 1 2 3; do
3573        //   for j in a b c; do
3574        //     if [ $i = "2" ] && [ $j = "b" ]; then continue 2; fi
3575        //     output="$output$i$j"
3576        //   done
3577        //   output="$output-"
3578        // done
3579        let inner_loop = Ast::For {
3580            variable: "j".to_string(),
3581            items: vec!["a".to_string(), "b".to_string(), "c".to_string()],
3582            body: Box::new(Ast::Sequence(vec![
3583                Ast::And {
3584                    left: Box::new(Ast::Pipeline(vec![ShellCommand {
3585                        args: vec!["test".to_string(), "$i".to_string(), "=".to_string(), "2".to_string()],
3586                        redirections: vec![],
3587                        compound: None,
3588                    }])),
3589                    right: Box::new(Ast::If {
3590                        branches: vec![(
3591                            Box::new(Ast::Pipeline(vec![ShellCommand {
3592                                args: vec!["test".to_string(), "$j".to_string(), "=".to_string(), "b".to_string()],
3593                                redirections: vec![],
3594                                compound: None,
3595                            }])),
3596                            Box::new(Ast::Pipeline(vec![ShellCommand {
3597                                args: vec!["continue".to_string(), "2".to_string()],
3598                                redirections: vec![],
3599                                compound: None,
3600                            }])),
3601                        )],
3602                        else_branch: None,
3603                    }),
3604                },
3605                Ast::Assignment {
3606                    var: "output".to_string(),
3607                    value: "$output$i$j".to_string(),
3608                },
3609            ])),
3610        };
3611        
3612        let outer_loop = Ast::For {
3613            variable: "i".to_string(),
3614            items: vec!["1".to_string(), "2".to_string(), "3".to_string()],
3615            body: Box::new(Ast::Sequence(vec![
3616                inner_loop,
3617                Ast::Assignment {
3618                    var: "output".to_string(),
3619                    value: "$output$i-".to_string(),
3620                },
3621            ])),
3622        };
3623        
3624        let exit_code = execute(outer_loop, &mut shell_state);
3625        assert_eq!(exit_code, 0);
3626        // After 2a, continue 2 skips rest of inner loop and the "$i-" assignment, goes to next outer iteration
3627        assert_eq!(shell_state.get_var("output"), Some("1a1b1c1-2a3a3b3c3-".to_string()));
3628    }
3629
3630    #[test]
3631    fn test_break_preserves_exit_code() {
3632        let mut shell_state = ShellState::new();
3633        
3634        // for i in 1 2 3; do
3635        //   false
3636        //   break
3637        // done
3638        // echo $?
3639        let ast = Ast::For {
3640            variable: "i".to_string(),
3641            items: vec!["1".to_string(), "2".to_string(), "3".to_string()],
3642            body: Box::new(Ast::Sequence(vec![
3643                Ast::Pipeline(vec![ShellCommand {
3644                    args: vec!["false".to_string()],
3645                    redirections: vec![],
3646                    compound: None,
3647                }]),
3648                Ast::Pipeline(vec![ShellCommand {
3649                    args: vec!["break".to_string()],
3650                    redirections: vec![],
3651                    compound: None,
3652                }]),
3653            ])),
3654        };
3655        
3656        let exit_code = execute(ast, &mut shell_state);
3657        // break returns 0, so the loop's exit code should be 0
3658        assert_eq!(exit_code, 0);
3659    }
3660
3661    #[test]
3662    fn test_continue_preserves_exit_code() {
3663        let mut shell_state = ShellState::new();
3664        shell_state.set_var("count", "0".to_string());
3665        
3666        // for i in 1 2; do
3667        //   count=$((count + 1))
3668        //   false
3669        //   continue
3670        // done
3671        let ast = Ast::For {
3672            variable: "i".to_string(),
3673            items: vec!["1".to_string(), "2".to_string()],
3674            body: Box::new(Ast::Sequence(vec![
3675                Ast::Assignment {
3676                    var: "count".to_string(),
3677                    value: "$((count + 1))".to_string(),
3678                },
3679                Ast::Pipeline(vec![ShellCommand {
3680                    args: vec!["false".to_string()],
3681                    redirections: vec![],
3682                    compound: None,
3683                }]),
3684                Ast::Pipeline(vec![ShellCommand {
3685                    args: vec!["continue".to_string()],
3686                    redirections: vec![],
3687                    compound: None,
3688                }]),
3689            ])),
3690        };
3691        
3692        let exit_code = execute(ast, &mut shell_state);
3693        // continue returns 0, so the loop's exit code should be 0
3694        assert_eq!(exit_code, 0);
3695        assert_eq!(shell_state.get_var("count"), Some("2".to_string()));
3696    }
3697
3698    // ========================================================================
3699    // Until Loop Tests
3700    // ========================================================================
3701
3702    #[test]
3703    fn test_until_basic_loop() {
3704        let mut shell_state = ShellState::new();
3705        shell_state.set_var("i", "0".to_string());
3706        shell_state.set_var("output", "".to_string());
3707        
3708        // i=0; until [ $i = "3" ]; do output="$output$i"; i=$((i + 1)); done
3709        let ast = Ast::Until {
3710            condition: Box::new(Ast::Pipeline(vec![ShellCommand {
3711                args: vec!["test".to_string(), "$i".to_string(), "=".to_string(), "3".to_string()],
3712                redirections: vec![],
3713                compound: None,
3714            }])),
3715            body: Box::new(Ast::Sequence(vec![
3716                Ast::Assignment {
3717                    var: "output".to_string(),
3718                    value: "$output$i".to_string(),
3719                },
3720                Ast::Assignment {
3721                    var: "i".to_string(),
3722                    value: "$((i + 1))".to_string(),
3723                },
3724            ])),
3725        };
3726        
3727        let exit_code = execute(ast, &mut shell_state);
3728        assert_eq!(exit_code, 0);
3729        assert_eq!(shell_state.get_var("output"), Some("012".to_string()));
3730        assert_eq!(shell_state.get_var("i"), Some("3".to_string()));
3731    }
3732
3733    #[test]
3734    fn test_until_condition_initially_true() {
3735        let mut shell_state = ShellState::new();
3736        shell_state.set_var("executed", "no".to_string());
3737        
3738        // until true; do executed="yes"; done
3739        let ast = Ast::Until {
3740            condition: Box::new(Ast::Pipeline(vec![ShellCommand {
3741                args: vec!["true".to_string()],
3742                redirections: vec![],
3743                compound: None,
3744            }])),
3745            body: Box::new(Ast::Assignment {
3746                var: "executed".to_string(),
3747                value: "yes".to_string(),
3748            }),
3749        };
3750        
3751        let exit_code = execute(ast, &mut shell_state);
3752        assert_eq!(exit_code, 0);
3753        // Body should not execute since condition is true (exit code 0)
3754        assert_eq!(shell_state.get_var("executed"), Some("no".to_string()));
3755    }
3756
3757    #[test]
3758    fn test_until_with_commands_in_body() {
3759        let mut shell_state = ShellState::new();
3760        shell_state.set_var("count", "0".to_string());
3761        
3762        // count=0; until [ $count -ge 3 ]; do count=$((count + 1)); echo $count; done
3763        let ast = Ast::Until {
3764            condition: Box::new(Ast::Pipeline(vec![ShellCommand {
3765                args: vec!["test".to_string(), "$count".to_string(), "-ge".to_string(), "3".to_string()],
3766                redirections: vec![],
3767                compound: None,
3768            }])),
3769            body: Box::new(Ast::Sequence(vec![
3770                Ast::Assignment {
3771                    var: "count".to_string(),
3772                    value: "$((count + 1))".to_string(),
3773                },
3774                Ast::Pipeline(vec![ShellCommand {
3775                    args: vec!["echo".to_string(), "$count".to_string()],
3776                    redirections: vec![],
3777                    compound: None,
3778                }]),
3779            ])),
3780        };
3781        
3782        let exit_code = execute(ast, &mut shell_state);
3783        assert_eq!(exit_code, 0);
3784        assert_eq!(shell_state.get_var("count"), Some("3".to_string()));
3785    }
3786
3787    #[test]
3788    fn test_until_with_variable_modification() {
3789        let mut shell_state = ShellState::new();
3790        shell_state.set_var("x", "1".to_string());
3791        
3792        // x=1; until [ $x -gt 5 ]; do x=$((x * 2)); done
3793        let ast = Ast::Until {
3794            condition: Box::new(Ast::Pipeline(vec![ShellCommand {
3795                args: vec!["test".to_string(), "$x".to_string(), "-gt".to_string(), "5".to_string()],
3796                redirections: vec![],
3797                compound: None,
3798            }])),
3799            body: Box::new(Ast::Assignment {
3800                var: "x".to_string(),
3801                value: "$((x * 2))".to_string(),
3802            }),
3803        };
3804        
3805        let exit_code = execute(ast, &mut shell_state);
3806        assert_eq!(exit_code, 0);
3807        assert_eq!(shell_state.get_var("x"), Some("8".to_string()));
3808    }
3809
3810    #[test]
3811    fn test_until_nested_loops() {
3812        let mut shell_state = ShellState::new();
3813        shell_state.set_var("output", "".to_string());
3814        shell_state.set_var("i", "0".to_string());
3815        
3816        let inner_loop = Ast::Until {
3817            condition: Box::new(Ast::Pipeline(vec![ShellCommand {
3818                args: vec!["test".to_string(), "$j".to_string(), "=".to_string(), "2".to_string()],
3819                redirections: vec![],
3820                compound: None,
3821            }])),
3822            body: Box::new(Ast::Sequence(vec![
3823                Ast::Assignment {
3824                    var: "output".to_string(),
3825                    value: "$output$i$j".to_string(),
3826                },
3827                Ast::Assignment {
3828                    var: "j".to_string(),
3829                    value: "$((j + 1))".to_string(),
3830                },
3831            ])),
3832        };
3833        
3834        let outer_loop = Ast::Until {
3835            condition: Box::new(Ast::Pipeline(vec![ShellCommand {
3836                args: vec!["test".to_string(), "$i".to_string(), "=".to_string(), "2".to_string()],
3837                redirections: vec![],
3838                compound: None,
3839            }])),
3840            body: Box::new(Ast::Sequence(vec![
3841                Ast::Assignment {
3842                    var: "i".to_string(),
3843                    value: "$((i + 1))".to_string(),
3844                },
3845                Ast::Assignment {
3846                    var: "j".to_string(),
3847                    value: "0".to_string(),
3848                },
3849                inner_loop,
3850            ])),
3851        };
3852        
3853        let exit_code = execute(outer_loop, &mut shell_state);
3854        assert_eq!(exit_code, 0);
3855        assert_eq!(shell_state.get_var("output"), Some("10112021".to_string()));
3856    }
3857
3858    #[test]
3859    fn test_until_with_break() {
3860        let mut shell_state = ShellState::new();
3861        shell_state.set_var("i", "0".to_string());
3862        shell_state.set_var("output", "".to_string());
3863        
3864        let ast = Ast::Until {
3865            condition: Box::new(Ast::Pipeline(vec![ShellCommand {
3866                args: vec!["false".to_string()],
3867                redirections: vec![],
3868                compound: None,
3869            }])),
3870            body: Box::new(Ast::Sequence(vec![
3871                Ast::Assignment {
3872                    var: "output".to_string(),
3873                    value: "$output$i".to_string(),
3874                },
3875                Ast::Assignment {
3876                    var: "i".to_string(),
3877                    value: "$((i + 1))".to_string(),
3878                },
3879                Ast::If {
3880                    branches: vec![(
3881                        Box::new(Ast::Pipeline(vec![ShellCommand {
3882                            args: vec!["test".to_string(), "$i".to_string(), "=".to_string(), "3".to_string()],
3883                            redirections: vec![],
3884                            compound: None,
3885                        }])),
3886                        Box::new(Ast::Pipeline(vec![ShellCommand {
3887                            args: vec!["break".to_string()],
3888                            redirections: vec![],
3889                            compound: None,
3890                        }])),
3891                    )],
3892                    else_branch: None,
3893                },
3894            ])),
3895        };
3896        
3897        let exit_code = execute(ast, &mut shell_state);
3898        assert_eq!(exit_code, 0);
3899        assert_eq!(shell_state.get_var("output"), Some("012".to_string()));
3900    }
3901
3902    #[test]
3903    fn test_until_with_continue() {
3904        let mut shell_state = ShellState::new();
3905        shell_state.set_var("i", "0".to_string());
3906        shell_state.set_var("output", "".to_string());
3907        
3908        let ast = Ast::Until {
3909            condition: Box::new(Ast::Pipeline(vec![ShellCommand {
3910                args: vec!["test".to_string(), "$i".to_string(), "-ge".to_string(), "5".to_string()],
3911                redirections: vec![],
3912                compound: None,
3913            }])),
3914            body: Box::new(Ast::Sequence(vec![
3915                Ast::Assignment {
3916                    var: "i".to_string(),
3917                    value: "$((i + 1))".to_string(),
3918                },
3919                Ast::If {
3920                    branches: vec![(
3921                        Box::new(Ast::Pipeline(vec![ShellCommand {
3922                            args: vec!["test".to_string(), "$i".to_string(), "=".to_string(), "3".to_string()],
3923                            redirections: vec![],
3924                            compound: None,
3925                        }])),
3926                        Box::new(Ast::Pipeline(vec![ShellCommand {
3927                            args: vec!["continue".to_string()],
3928                            redirections: vec![],
3929                            compound: None,
3930                        }])),
3931                    )],
3932                    else_branch: None,
3933                },
3934                Ast::Assignment {
3935                    var: "output".to_string(),
3936                    value: "$output$i".to_string(),
3937                },
3938            ])),
3939        };
3940        
3941        let exit_code = execute(ast, &mut shell_state);
3942        assert_eq!(exit_code, 0);
3943        assert_eq!(shell_state.get_var("output"), Some("1245".to_string()));
3944    }
3945
3946    #[test]
3947    fn test_until_empty_body() {
3948        let mut shell_state = ShellState::new();
3949        shell_state.set_var("i", "0".to_string());
3950        
3951        // until true; do :; done (empty body with true condition)
3952        let ast = Ast::Until {
3953            condition: Box::new(Ast::Pipeline(vec![ShellCommand {
3954                args: vec!["true".to_string()],
3955                redirections: vec![],
3956                compound: None,
3957            }])),
3958            body: Box::new(Ast::Pipeline(vec![ShellCommand {
3959                args: vec!["true".to_string()],
3960                redirections: vec![],
3961                compound: None,
3962            }])),
3963        };
3964        
3965        let exit_code = execute(ast, &mut shell_state);
3966        assert_eq!(exit_code, 0);
3967    }
3968
3969    #[test]
3970    fn test_until_with_command_substitution() {
3971        let mut shell_state = ShellState::new();
3972        shell_state.set_var("count", "0".to_string());
3973        shell_state.set_var("output", "".to_string());
3974        
3975        // until [ $(echo $count) = "3" ]; do output="$output$count"; count=$((count + 1)); done
3976        let ast = Ast::Until {
3977            condition: Box::new(Ast::Pipeline(vec![ShellCommand {
3978                args: vec!["test".to_string(), "$(echo $count)".to_string(), "=".to_string(), "3".to_string()],
3979                redirections: vec![],
3980                compound: None,
3981            }])),
3982            body: Box::new(Ast::Sequence(vec![
3983                Ast::Assignment {
3984                    var: "output".to_string(),
3985                    value: "$output$count".to_string(),
3986                },
3987                Ast::Assignment {
3988                    var: "count".to_string(),
3989                    value: "$((count + 1))".to_string(),
3990                },
3991            ])),
3992        };
3993        
3994        let exit_code = execute(ast, &mut shell_state);
3995        assert_eq!(exit_code, 0);
3996        assert_eq!(shell_state.get_var("output"), Some("012".to_string()));
3997    }
3998
3999    #[test]
4000    fn test_until_with_arithmetic_condition() {
4001        let mut shell_state = ShellState::new();
4002        shell_state.set_var("x", "1".to_string());
4003        shell_state.set_var("output", "".to_string());
4004        
4005        // x=1; until [ $((x * 2)) -gt 10 ]; do output="$output$x"; x=$((x + 1)); done
4006        let ast = Ast::Until {
4007            condition: Box::new(Ast::Pipeline(vec![ShellCommand {
4008                args: vec!["test".to_string(), "$((x * 2))".to_string(), "-gt".to_string(), "10".to_string()],
4009                redirections: vec![],
4010                compound: None,
4011            }])),
4012            body: Box::new(Ast::Sequence(vec![
4013                Ast::Assignment {
4014                    var: "output".to_string(),
4015                    value: "$output$x".to_string(),
4016                },
4017                Ast::Assignment {
4018                    var: "x".to_string(),
4019                    value: "$((x + 1))".to_string(),
4020                },
4021            ])),
4022        };
4023        
4024        let exit_code = execute(ast, &mut shell_state);
4025        assert_eq!(exit_code, 0);
4026        assert_eq!(shell_state.get_var("output"), Some("12345".to_string()));
4027    }
4028
4029    #[test]
4030    fn test_until_inside_for() {
4031        let mut shell_state = ShellState::new();
4032        shell_state.set_var("output", "".to_string());
4033        
4034        // for i in 1 2; do j=0; until [ $j = "2" ]; do output="$output$i$j"; j=$((j + 1)); done; done
4035        let inner_until = Ast::Until {
4036            condition: Box::new(Ast::Pipeline(vec![ShellCommand {
4037                args: vec!["test".to_string(), "$j".to_string(), "=".to_string(), "2".to_string()],
4038                redirections: vec![],
4039                compound: None,
4040            }])),
4041            body: Box::new(Ast::Sequence(vec![
4042                Ast::Assignment {
4043                    var: "output".to_string(),
4044                    value: "$output$i$j".to_string(),
4045                },
4046                Ast::Assignment {
4047                    var: "j".to_string(),
4048                    value: "$((j + 1))".to_string(),
4049                },
4050            ])),
4051        };
4052        
4053        let outer_for = Ast::For {
4054            variable: "i".to_string(),
4055            items: vec!["1".to_string(), "2".to_string()],
4056            body: Box::new(Ast::Sequence(vec![
4057                Ast::Assignment {
4058                    var: "j".to_string(),
4059                    value: "0".to_string(),
4060                },
4061                inner_until,
4062            ])),
4063        };
4064        
4065        let exit_code = execute(outer_for, &mut shell_state);
4066        assert_eq!(exit_code, 0);
4067        assert_eq!(shell_state.get_var("output"), Some("10112021".to_string()));
4068    }
4069
4070    #[test]
4071    fn test_for_inside_until() {
4072        let mut shell_state = ShellState::new();
4073        shell_state.set_var("output", "".to_string());
4074        shell_state.set_var("i", "0".to_string());
4075        
4076        // i=0; until [ $i = "2" ]; do for j in a b; do output="$output$i$j"; done; i=$((i + 1)); done
4077        let inner_for = Ast::For {
4078            variable: "j".to_string(),
4079            items: vec!["a".to_string(), "b".to_string()],
4080            body: Box::new(Ast::Assignment {
4081                var: "output".to_string(),
4082                value: "$output$i$j".to_string(),
4083            }),
4084        };
4085        
4086        let outer_until = Ast::Until {
4087            condition: Box::new(Ast::Pipeline(vec![ShellCommand {
4088                args: vec!["test".to_string(), "$i".to_string(), "=".to_string(), "2".to_string()],
4089                redirections: vec![],
4090                compound: None,
4091            }])),
4092            body: Box::new(Ast::Sequence(vec![
4093                inner_for,
4094                Ast::Assignment {
4095                    var: "i".to_string(),
4096                    value: "$((i + 1))".to_string(),
4097                },
4098            ])),
4099        };
4100        
4101        let exit_code = execute(outer_until, &mut shell_state);
4102        assert_eq!(exit_code, 0);
4103        assert_eq!(shell_state.get_var("output"), Some("0a0b1a1b".to_string()));
4104    }
4105
4106    #[test]
4107    fn test_until_inside_while() {
4108        let mut shell_state = ShellState::new();
4109        shell_state.set_var("output", "".to_string());
4110        shell_state.set_var("i", "0".to_string());
4111        
4112        let inner_until = Ast::Until {
4113            condition: Box::new(Ast::Pipeline(vec![ShellCommand {
4114                args: vec!["test".to_string(), "$j".to_string(), "=".to_string(), "2".to_string()],
4115                redirections: vec![],
4116                compound: None,
4117            }])),
4118            body: Box::new(Ast::Sequence(vec![
4119                Ast::Assignment {
4120                    var: "output".to_string(),
4121                    value: "$output$i$j".to_string(),
4122                },
4123                Ast::Assignment {
4124                    var: "j".to_string(),
4125                    value: "$((j + 1))".to_string(),
4126                },
4127            ])),
4128        };
4129        
4130        let outer_while = Ast::While {
4131            condition: Box::new(Ast::Pipeline(vec![ShellCommand {
4132                args: vec!["test".to_string(), "$i".to_string(), "-lt".to_string(), "2".to_string()],
4133                redirections: vec![],
4134                compound: None,
4135            }])),
4136            body: Box::new(Ast::Sequence(vec![
4137                Ast::Assignment {
4138                    var: "i".to_string(),
4139                    value: "$((i + 1))".to_string(),
4140                },
4141                Ast::Assignment {
4142                    var: "j".to_string(),
4143                    value: "0".to_string(),
4144                },
4145                inner_until,
4146            ])),
4147        };
4148        
4149        let exit_code = execute(outer_while, &mut shell_state);
4150        assert_eq!(exit_code, 0);
4151        assert_eq!(shell_state.get_var("output"), Some("10112021".to_string()));
4152    }
4153
4154    #[test]
4155    fn test_while_inside_until() {
4156        let mut shell_state = ShellState::new();
4157        shell_state.set_var("output", "".to_string());
4158        shell_state.set_var("i", "0".to_string());
4159        
4160        let inner_while = Ast::While {
4161            condition: Box::new(Ast::Pipeline(vec![ShellCommand {
4162                args: vec!["test".to_string(), "$j".to_string(), "-lt".to_string(), "2".to_string()],
4163                redirections: vec![],
4164                compound: None,
4165            }])),
4166            body: Box::new(Ast::Sequence(vec![
4167                Ast::Assignment {
4168                    var: "output".to_string(),
4169                    value: "$output$i$j".to_string(),
4170                },
4171                Ast::Assignment {
4172                    var: "j".to_string(),
4173                    value: "$((j + 1))".to_string(),
4174                },
4175            ])),
4176        };
4177        
4178        let outer_until = Ast::Until {
4179            condition: Box::new(Ast::Pipeline(vec![ShellCommand {
4180                args: vec!["test".to_string(), "$i".to_string(), "=".to_string(), "2".to_string()],
4181                redirections: vec![],
4182                compound: None,
4183            }])),
4184            body: Box::new(Ast::Sequence(vec![
4185                Ast::Assignment {
4186                    var: "i".to_string(),
4187                    value: "$((i + 1))".to_string(),
4188                },
4189                Ast::Assignment {
4190                    var: "j".to_string(),
4191                    value: "0".to_string(),
4192                },
4193                inner_while,
4194            ])),
4195        };
4196        
4197        let exit_code = execute(outer_until, &mut shell_state);
4198        assert_eq!(exit_code, 0);
4199        assert_eq!(shell_state.get_var("output"), Some("10112021".to_string()));
4200    }
4201
4202    #[test]
4203    fn test_until_preserves_exit_code() {
4204        let mut shell_state = ShellState::new();
4205        shell_state.set_var("i", "0".to_string());
4206        
4207        // until [ $i = "1" ]; do i=$((i + 1)); false; done
4208        let ast = Ast::Until {
4209            condition: Box::new(Ast::Pipeline(vec![ShellCommand {
4210                args: vec!["test".to_string(), "$i".to_string(), "=".to_string(), "1".to_string()],
4211                redirections: vec![],
4212                compound: None,
4213            }])),
4214            body: Box::new(Ast::Sequence(vec![
4215                Ast::Assignment {
4216                    var: "i".to_string(),
4217                    value: "$((i + 1))".to_string(),
4218                },
4219                Ast::Pipeline(vec![ShellCommand {
4220                    args: vec!["false".to_string()],
4221                    redirections: vec![],
4222                    compound: None,
4223                }]),
4224            ])),
4225        };
4226        
4227        let exit_code = execute(ast, &mut shell_state);
4228        // Last command in body was false (exit 1), so loop should return 1
4229        assert_eq!(exit_code, 1);
4230    }
4231}