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