rush_sh/
executor.rs

1use std::cell::RefCell;
2use std::fs::{File, OpenOptions};
3use std::io::{BufRead, BufReader, Write, pipe};
4use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd, RawFd};
5use std::os::unix::process::CommandExt;
6use std::process::{Command, Stdio};
7use std::rc::Rc;
8
9use super::parser::{Ast, Redirection, ShellCommand};
10use super::state::ShellState;
11
12/// Maximum allowed subshell nesting depth to prevent stack overflow
13const MAX_SUBSHELL_DEPTH: usize = 100;
14
15/// Execute a command and capture its output as a string
16/// This is used for command substitution $(...)
17fn execute_and_capture_output(ast: Ast, shell_state: &mut ShellState) -> Result<String, String> {
18    // Create a pipe to capture stdout
19    let (reader, writer) = pipe().map_err(|e| format!("Failed to create pipe: {}", e))?;
20
21    // We need to capture the output, so we'll redirect stdout to our pipe
22    // For builtins, we can pass the writer directly
23    // For external commands, we need to handle them specially
24
25    match &ast {
26        Ast::Pipeline(commands) => {
27            // Handle both single commands and multi-command pipelines
28            if commands.is_empty() {
29                return Ok(String::new());
30            }
31
32            if commands.len() == 1 {
33                // Single command - use the existing optimized path
34                let cmd = &commands[0];
35                if cmd.args.is_empty() {
36                    return Ok(String::new());
37                }
38
39                // Expand variables and wildcards
40                let var_expanded_args = expand_variables_in_args(&cmd.args, shell_state);
41                let expanded_args = expand_wildcards(&var_expanded_args)
42                    .map_err(|e| format!("Wildcard expansion failed: {}", e))?;
43
44                if expanded_args.is_empty() {
45                    return Ok(String::new());
46                }
47
48                // Check if it's a function call
49                if shell_state.get_function(&expanded_args[0]).is_some() {
50                    // Save previous capture state (for nested command substitutions)
51                    let previous_capture = shell_state.capture_output.clone();
52
53                    // Enable output capture mode
54                    let capture_buffer = Rc::new(RefCell::new(Vec::new()));
55                    shell_state.capture_output = Some(capture_buffer.clone());
56
57                    // Create a FunctionCall AST and execute it
58                    let function_call_ast = Ast::FunctionCall {
59                        name: expanded_args[0].clone(),
60                        args: expanded_args[1..].to_vec(),
61                    };
62
63                    let exit_code = execute(function_call_ast, shell_state);
64
65                    // Retrieve captured output
66                    let captured = capture_buffer.borrow().clone();
67                    let output = String::from_utf8_lossy(&captured).trim_end().to_string();
68
69                    // Restore previous capture state
70                    shell_state.capture_output = previous_capture;
71
72                    if exit_code == 0 {
73                        Ok(output)
74                    } else {
75                        Err(format!("Function failed with exit code {}", exit_code))
76                    }
77                } else if crate::builtins::is_builtin(&expanded_args[0]) {
78                    let temp_cmd = ShellCommand {
79                        args: expanded_args,
80                        redirections: cmd.redirections.clone(),
81                        compound: None,
82                    };
83
84                    // Execute builtin with our writer
85                    let exit_code = crate::builtins::execute_builtin(
86                        &temp_cmd,
87                        shell_state,
88                        Some(Box::new(writer)),
89                    );
90
91                    // Read the captured output
92                    drop(temp_cmd); // Ensure writer is dropped
93                    let mut output = String::new();
94                    use std::io::Read;
95                    let mut reader = reader;
96                    reader
97                        .read_to_string(&mut output)
98                        .map_err(|e| format!("Failed to read output: {}", e))?;
99
100                    if exit_code == 0 {
101                        Ok(output.trim_end().to_string())
102                    } else {
103                        Err(format!("Command failed with exit code {}", exit_code))
104                    }
105                } else {
106                    // External command - execute with output capture
107                    drop(writer); // Close writer end before spawning
108
109                    let mut command = Command::new(&expanded_args[0]);
110                    command.args(&expanded_args[1..]);
111                    command.stdout(Stdio::piped());
112                    command.stderr(Stdio::null()); // Suppress stderr for command substitution
113
114                    // Set environment
115                    let child_env = shell_state.get_env_for_child();
116                    command.env_clear();
117                    for (key, value) in child_env {
118                        command.env(key, value);
119                    }
120
121                    let output = command
122                        .output()
123                        .map_err(|e| format!("Failed to execute command: {}", e))?;
124
125                    if output.status.success() {
126                        Ok(String::from_utf8_lossy(&output.stdout)
127                            .trim_end()
128                            .to_string())
129                    } else {
130                        Err(format!(
131                            "Command failed with exit code {}",
132                            output.status.code().unwrap_or(1)
133                        ))
134                    }
135                }
136            } else {
137                // Multi-command pipeline - execute the entire pipeline and capture output
138                drop(writer); // Close writer end before executing pipeline
139
140                // Save previous capture state (for nested command substitutions)
141                let previous_capture = shell_state.capture_output.clone();
142
143                // Enable output capture mode
144                let capture_buffer = Rc::new(RefCell::new(Vec::new()));
145                shell_state.capture_output = Some(capture_buffer.clone());
146
147                // Execute the pipeline
148                let exit_code = execute_pipeline(commands, shell_state);
149
150                // Retrieve captured output
151                let captured = capture_buffer.borrow().clone();
152                let output = String::from_utf8_lossy(&captured).trim_end().to_string();
153
154                // Restore previous capture state
155                shell_state.capture_output = previous_capture;
156
157                if exit_code == 0 {
158                    Ok(output)
159                } else {
160                    Err(format!("Pipeline failed with exit code {}", exit_code))
161                }
162            }
163        }
164        _ => {
165            // For other AST nodes (sequences, etc.), we need special handling
166            drop(writer);
167
168            // Save previous capture state
169            let previous_capture = shell_state.capture_output.clone();
170
171            // Enable output capture mode
172            let capture_buffer = Rc::new(RefCell::new(Vec::new()));
173            shell_state.capture_output = Some(capture_buffer.clone());
174
175            // Execute the AST
176            let exit_code = execute(ast, shell_state);
177
178            // Retrieve captured output
179            let captured = capture_buffer.borrow().clone();
180            let output = String::from_utf8_lossy(&captured).trim_end().to_string();
181
182            // Restore previous capture state
183            shell_state.capture_output = previous_capture;
184
185            if exit_code == 0 {
186                Ok(output)
187            } else {
188                Err(format!("Command failed with exit code {}", exit_code))
189            }
190        }
191    }
192}
193
194fn expand_variables_in_args(args: &[String], shell_state: &mut ShellState) -> Vec<String> {
195    let mut expanded_args = Vec::new();
196
197    for arg in args {
198        // Expand variables within the argument string
199        let expanded_arg = expand_variables_in_string(arg, shell_state);
200        expanded_args.push(expanded_arg);
201    }
202
203    expanded_args
204}
205
206pub fn expand_variables_in_string(input: &str, shell_state: &mut ShellState) -> String {
207    let mut result = String::new();
208    let mut chars = input.chars().peekable();
209
210    while let Some(ch) = chars.next() {
211        if ch == '$' {
212            // Check for command substitution $(...) or arithmetic expansion $((...))
213            if let Some(&'(') = chars.peek() {
214                chars.next(); // consume first (
215
216                // Check if this is arithmetic expansion $((...))
217                if let Some(&'(') = chars.peek() {
218                    // Arithmetic expansion $((...))
219                    chars.next(); // consume second (
220                    let mut arithmetic_expr = String::new();
221                    let mut paren_depth = 1;
222                    let mut found_closing = false;
223
224                    while let Some(c) = chars.next() {
225                        if c == '(' {
226                            paren_depth += 1;
227                            arithmetic_expr.push(c);
228                        } else if c == ')' {
229                            paren_depth -= 1;
230                            if paren_depth == 0 {
231                                // Found the first closing ) - check for second )
232                                if let Some(&')') = chars.peek() {
233                                    chars.next(); // consume the second )
234                                    found_closing = true;
235                                    break;
236                                } else {
237                                    // Missing second closing paren, treat as error
238                                    result.push_str("$((");
239                                    result.push_str(&arithmetic_expr);
240                                    result.push(')');
241                                    break;
242                                }
243                            }
244                            arithmetic_expr.push(c);
245                        } else {
246                            arithmetic_expr.push(c);
247                        }
248                    }
249
250                    if found_closing {
251                        // First expand variables in the arithmetic expression
252                        // The arithmetic evaluator expects variable names without $ prefix
253                        // So we need to expand $VAR to the value before evaluation
254                        let mut expanded_expr = String::new();
255                        let mut expr_chars = arithmetic_expr.chars().peekable();
256
257                        while let Some(ch) = expr_chars.next() {
258                            if ch == '$' {
259                                // Expand variable
260                                let mut var_name = String::new();
261                                if let Some(&c) = expr_chars.peek() {
262                                    if c == '?'
263                                        || c == '$'
264                                        || c == '0'
265                                        || c == '#'
266                                        || c == '*'
267                                        || c == '@'
268                                        || c.is_ascii_digit()
269                                    {
270                                        var_name.push(c);
271                                        expr_chars.next();
272                                    } else {
273                                        while let Some(&c) = expr_chars.peek() {
274                                            if c.is_alphanumeric() || c == '_' {
275                                                var_name.push(c);
276                                                expr_chars.next();
277                                            } else {
278                                                break;
279                                            }
280                                        }
281                                    }
282                                }
283
284                                if !var_name.is_empty() {
285                                    if let Some(value) = shell_state.get_var(&var_name) {
286                                        expanded_expr.push_str(&value);
287                                    } else {
288                                        // Variable not found, use 0 for arithmetic
289                                        expanded_expr.push('0');
290                                    }
291                                } else {
292                                    expanded_expr.push('$');
293                                }
294                            } else {
295                                expanded_expr.push(ch);
296                            }
297                        }
298
299                        match crate::arithmetic::evaluate_arithmetic_expression(
300                            &expanded_expr,
301                            shell_state,
302                        ) {
303                            Ok(value) => {
304                                result.push_str(&value.to_string());
305                            }
306                            Err(e) => {
307                                // On arithmetic error, display a proper error message
308                                if shell_state.colors_enabled {
309                                    result.push_str(&format!(
310                                        "{}arithmetic error: {}{}",
311                                        shell_state.color_scheme.error, e, "\x1b[0m"
312                                    ));
313                                } else {
314                                    result.push_str(&format!("arithmetic error: {}", e));
315                                }
316                            }
317                        }
318                    } else {
319                        // Didn't find proper closing - keep as literal
320                        result.push_str("$((");
321                        result.push_str(&arithmetic_expr);
322                        // Note: we don't add closing parens since they weren't in the input
323                    }
324                    continue;
325                }
326
327                // Regular command substitution $(...)
328                let mut sub_command = String::new();
329                let mut paren_depth = 1;
330
331                for c in chars.by_ref() {
332                    if c == '(' {
333                        paren_depth += 1;
334                        sub_command.push(c);
335                    } else if c == ')' {
336                        paren_depth -= 1;
337                        if paren_depth == 0 {
338                            break;
339                        }
340                        sub_command.push(c);
341                    } else {
342                        sub_command.push(c);
343                    }
344                }
345
346                // Execute the command substitution within the current shell context
347                // Parse and execute the command using our own lexer/parser/executor
348                if let Ok(tokens) = crate::lexer::lex(&sub_command, shell_state) {
349                    // Expand aliases before parsing
350                    let expanded_tokens = match crate::lexer::expand_aliases(
351                        tokens,
352                        shell_state,
353                        &mut std::collections::HashSet::new(),
354                    ) {
355                        Ok(t) => t,
356                        Err(_) => {
357                            // Alias expansion error, keep literal
358                            result.push_str("$(");
359                            result.push_str(&sub_command);
360                            result.push(')');
361                            continue;
362                        }
363                    };
364
365                    match crate::parser::parse(expanded_tokens) {
366                        Ok(ast) => {
367                            // Execute within current shell context and capture output
368                            match execute_and_capture_output(ast, shell_state) {
369                                Ok(output) => {
370                                    result.push_str(&output);
371                                }
372                                Err(_) => {
373                                    // On failure, keep the literal
374                                    result.push_str("$(");
375                                    result.push_str(&sub_command);
376                                    result.push(')');
377                                }
378                            }
379                        }
380                        Err(_parse_err) => {
381                            // Parse error - try to handle as function call if it looks like one
382                            let tokens_str = sub_command.trim();
383                            if tokens_str.contains(' ') {
384                                // Split by spaces and check if first token looks like a function call
385                                let parts: Vec<&str> = tokens_str.split_whitespace().collect();
386                                if let Some(first_token) = parts.first()
387                                    && shell_state.get_function(first_token).is_some()
388                                {
389                                    // This is a function call, create AST manually
390                                    let function_call = Ast::FunctionCall {
391                                        name: first_token.to_string(),
392                                        args: parts[1..].iter().map(|s| s.to_string()).collect(),
393                                    };
394                                    match execute_and_capture_output(function_call, shell_state) {
395                                        Ok(output) => {
396                                            result.push_str(&output);
397                                            continue;
398                                        }
399                                        Err(_) => {
400                                            // Fall back to literal
401                                        }
402                                    }
403                                }
404                            }
405                            // Keep the literal
406                            result.push_str("$(");
407                            result.push_str(&sub_command);
408                            result.push(')');
409                        }
410                    }
411                } else {
412                    // Lex error, keep literal
413                    result.push_str("$(");
414                    result.push_str(&sub_command);
415                    result.push(')');
416                }
417            } else {
418                // Regular variable
419                let mut var_name = String::new();
420                let mut next_ch = chars.peek();
421
422                // Handle special single-character variables first
423                if let Some(&c) = next_ch {
424                    if c == '?' || c == '$' || c == '0' || c == '#' || c == '*' || c == '@' {
425                        var_name.push(c);
426                        chars.next(); // consume the character
427                    } else if c.is_ascii_digit() {
428                        // Positional parameter
429                        var_name.push(c);
430                        chars.next();
431                    } else {
432                        // Regular variable name
433                        while let Some(&c) = next_ch {
434                            if c.is_alphanumeric() || c == '_' {
435                                var_name.push(c);
436                                chars.next(); // consume the character
437                                next_ch = chars.peek();
438                            } else {
439                                break;
440                            }
441                        }
442                    }
443                }
444
445                if !var_name.is_empty() {
446                    if let Some(value) = shell_state.get_var(&var_name) {
447                        result.push_str(&value);
448                    } else {
449                        // Variable not found - for positional parameters, expand to empty string
450                        // For other variables, keep the literal
451                        if var_name.chars().next().unwrap().is_ascii_digit()
452                            || var_name == "?"
453                            || var_name == "$"
454                            || var_name == "0"
455                            || var_name == "#"
456                            || var_name == "*"
457                            || var_name == "@"
458                        {
459                            // Expand to empty string for undefined positional parameters
460                        } else {
461                            // Keep the literal for regular variables
462                            result.push('$');
463                            result.push_str(&var_name);
464                        }
465                    }
466                } else {
467                    result.push('$');
468                }
469            }
470        } else if ch == '`' {
471            // Backtick command substitution
472            let mut sub_command = String::new();
473
474            for c in chars.by_ref() {
475                if c == '`' {
476                    break;
477                }
478                sub_command.push(c);
479            }
480
481            // Execute the command substitution
482            if let Ok(tokens) = crate::lexer::lex(&sub_command, shell_state) {
483                // Expand aliases before parsing
484                let expanded_tokens = match crate::lexer::expand_aliases(
485                    tokens,
486                    shell_state,
487                    &mut std::collections::HashSet::new(),
488                ) {
489                    Ok(t) => t,
490                    Err(_) => {
491                        // Alias expansion error, keep literal
492                        result.push('`');
493                        result.push_str(&sub_command);
494                        result.push('`');
495                        continue;
496                    }
497                };
498
499                if let Ok(ast) = crate::parser::parse(expanded_tokens) {
500                    // Execute and capture output
501                    match execute_and_capture_output(ast, shell_state) {
502                        Ok(output) => {
503                            result.push_str(&output);
504                        }
505                        Err(_) => {
506                            // On failure, keep the literal
507                            result.push('`');
508                            result.push_str(&sub_command);
509                            result.push('`');
510                        }
511                    }
512                } else {
513                    // Parse error, keep literal
514                    result.push('`');
515                    result.push_str(&sub_command);
516                    result.push('`');
517                }
518            } else {
519                // Lex error, keep literal
520                result.push('`');
521                result.push_str(&sub_command);
522                result.push('`');
523            }
524        } else {
525            result.push(ch);
526        }
527    }
528
529    result
530}
531
532fn expand_wildcards(args: &[String]) -> Result<Vec<String>, String> {
533    let mut expanded_args = Vec::new();
534
535    for arg in args {
536        if arg.contains('*') || arg.contains('?') || arg.contains('[') {
537            // Try to expand wildcard
538            match glob::glob(arg) {
539                Ok(paths) => {
540                    let mut matches: Vec<String> = paths
541                        .filter_map(|p| p.ok())
542                        .map(|p| p.to_string_lossy().to_string())
543                        .collect();
544                    if matches.is_empty() {
545                        // No matches, keep literal
546                        expanded_args.push(arg.clone());
547                    } else {
548                        // Sort for consistent behavior
549                        matches.sort();
550                        expanded_args.extend(matches);
551                    }
552                }
553                Err(_e) => {
554                    // Invalid pattern, keep literal
555                    expanded_args.push(arg.clone());
556                }
557            }
558        } else {
559            expanded_args.push(arg.clone());
560        }
561    }
562    Ok(expanded_args)
563}
564
565/// Collect here-document content from stdin until the specified delimiter is found
566/// This function reads from stdin line by line until it finds a line that exactly matches the delimiter
567/// If shell_state has pending_heredoc_content, it uses that instead (for script execution)
568fn collect_here_document_content(delimiter: &str, shell_state: &mut ShellState) -> String {
569    // Check if we have pending here-document content from script execution
570    if let Some(content) = shell_state.pending_heredoc_content.take() {
571        return content;
572    }
573
574    // Otherwise, read from stdin (interactive mode)
575    let stdin = std::io::stdin();
576    let mut reader = BufReader::new(stdin.lock());
577    let mut content = String::new();
578    let mut line = String::new();
579
580    loop {
581        line.clear();
582        match reader.read_line(&mut line) {
583            Ok(0) => {
584                // EOF reached
585                break;
586            }
587            Ok(_) => {
588                // Check if this line (without trailing newline) matches the delimiter
589                let line_content = line.trim_end();
590                if line_content == delimiter {
591                    // Found the delimiter, stop collecting
592                    break;
593                } else {
594                    // This is content, add it to our collection
595                    content.push_str(&line);
596                }
597            }
598            Err(e) => {
599                if shell_state.colors_enabled {
600                    eprintln!(
601                        "{}Error reading here-document content: {}\x1b[0m",
602                        shell_state.color_scheme.error, e
603                    );
604                } else {
605                    eprintln!("Error reading here-document content: {}", e);
606                }
607                break;
608            }
609        }
610    }
611
612    content
613}
614
615/// Apply all redirections for a command in left-to-right order (POSIX requirement)
616///
617/// # Arguments
618/// * `redirections` - List of redirections to apply
619/// * `shell_state` - Mutable reference to shell state
620/// * `command` - Optional mutable reference to Command (for external commands)
621///
622/// # Returns
623/// * `Ok(())` on success
624/// * `Err(String)` with error message on failure
625fn apply_redirections(
626    redirections: &[Redirection],
627    shell_state: &mut ShellState,
628    mut command: Option<&mut Command>,
629) -> Result<(), String> {
630    // Process redirections in left-to-right order per POSIX
631    for redir in redirections {
632        match redir {
633            Redirection::Input(file) => {
634                apply_input_redirection(0, file, shell_state, command.as_deref_mut())?;
635            }
636            Redirection::Output(file) => {
637                apply_output_redirection(1, file, false, shell_state, command.as_deref_mut())?;
638            }
639            Redirection::Append(file) => {
640                apply_output_redirection(1, file, true, shell_state, command.as_deref_mut())?;
641            }
642            Redirection::FdInput(fd, file) => {
643                apply_input_redirection(*fd, file, shell_state, command.as_deref_mut())?;
644            }
645            Redirection::FdOutput(fd, file) => {
646                apply_output_redirection(*fd, file, false, shell_state, command.as_deref_mut())?;
647            }
648            Redirection::FdAppend(fd, file) => {
649                apply_output_redirection(*fd, file, true, shell_state, command.as_deref_mut())?;
650            }
651            Redirection::FdDuplicate(target_fd, source_fd) => {
652                apply_fd_duplication(*target_fd, *source_fd, shell_state, command.as_deref_mut())?;
653            }
654            Redirection::FdClose(fd) => {
655                apply_fd_close(*fd, shell_state, command.as_deref_mut())?;
656            }
657            Redirection::FdInputOutput(fd, file) => {
658                apply_fd_input_output(*fd, file, shell_state, command.as_deref_mut())?;
659            }
660            Redirection::HereDoc(delimiter, quoted_str) => {
661                let quoted = quoted_str == "true";
662                apply_heredoc_redirection(
663                    0,
664                    delimiter,
665                    quoted,
666                    shell_state,
667                    command.as_deref_mut(),
668                )?;
669            }
670            Redirection::HereString(content) => {
671                apply_herestring_redirection(0, content, shell_state, command.as_deref_mut())?;
672            }
673        }
674    }
675    Ok(())
676}
677
678/// Apply input redirection for a specific file descriptor
679fn apply_input_redirection(
680    fd: i32,
681    file: &str,
682    shell_state: &mut ShellState,
683    command: Option<&mut Command>,
684) -> Result<(), String> {
685    let expanded_file = expand_variables_in_string(file, shell_state);
686
687    // Open file for reading
688    let file_handle =
689        File::open(&expanded_file).map_err(|e| format!("Cannot open {}: {}", expanded_file, e))?;
690
691    if fd == 0 {
692        // stdin redirection - apply to Command if present
693        if let Some(cmd) = command {
694            cmd.stdin(Stdio::from(file_handle));
695        } else {
696            // For builtins or command groups (command is None), redirect shell's stdin
697            shell_state.fd_table.borrow_mut().open_fd(
698                0,
699                &expanded_file,
700                true,  // read
701                false, // write
702                false, // append
703                false, // truncate
704            )?;
705
706            // Also perform OS-level dup2
707            let raw_fd = shell_state.fd_table.borrow().get_raw_fd(0);
708            if let Some(rfd) = raw_fd {
709                if rfd != 0 {
710                    unsafe {
711                        if libc::dup2(rfd, 0) < 0 {
712                            return Err(format!("Failed to dup2 fd {} to 0", rfd));
713                        }
714                    }
715                }
716            }
717        }
718    } else {
719        // Custom fd - for external commands, we need to redirect the custom fd for reading
720        // Open the file (we need to keep the handle alive for the command)
721        let fd_file = File::open(&expanded_file)
722            .map_err(|e| format!("Cannot open {}: {}", expanded_file, e))?;
723
724        // For external commands, store both in fd table and prepare for stdin redirect
725        shell_state.fd_table.borrow_mut().open_fd(
726            fd,
727            &expanded_file,
728            true,  // read
729            false, // write
730            false, // append
731            false, // truncate
732        )?;
733
734        // If we have an external command, set up the file descriptor in the child process
735        if let Some(cmd) = command {
736            // Keep fd_file alive by moving it into the closure
737            // It will be dropped (and closed) when the closure is dropped in the parent
738            let target_fd = fd;
739            unsafe {
740                cmd.pre_exec(move || {
741                    let raw_fd = fd_file.as_raw_fd();
742
743                    // The inherited file descriptor might not be at the target fd number
744                    // Use dup2 to ensure it's at the correct fd number
745                    if raw_fd != target_fd {
746                        let result = libc::dup2(raw_fd, target_fd);
747                        if result < 0 {
748                            return Err(std::io::Error::last_os_error());
749                        }
750                        // We don't need to close raw_fd manually because fd_file
751                        // has CLOEXEC set by default and will be closed on exec
752                    }
753                    Ok(())
754                });
755            }
756        }
757    }
758
759    Ok(())
760}
761
762/// Apply output redirection for a specific file descriptor
763fn apply_output_redirection(
764    fd: i32,
765    file: &str,
766    append: bool,
767    shell_state: &mut ShellState,
768    command: Option<&mut Command>,
769) -> Result<(), String> {
770    let expanded_file = expand_variables_in_string(file, shell_state);
771
772    // Open file for writing or appending
773    let file_handle = if append {
774        OpenOptions::new()
775            .append(true)
776            .create(true)
777            .open(&expanded_file)
778            .map_err(|e| format!("Cannot open {}: {}", expanded_file, e))?
779    } else {
780        File::create(&expanded_file)
781            .map_err(|e| format!("Cannot create {}: {}", expanded_file, e))?
782    };
783
784    if let Some(cmd) = command {
785        if fd == 1 {
786            // stdout redirection - apply to Command if present
787            cmd.stdout(Stdio::from(file_handle));
788        } else if fd == 2 {
789            // stderr redirection - apply to Command if present
790            cmd.stderr(Stdio::from(file_handle));
791        } else {
792            // Custom fd - store in fd table (and pre_exec will handle it?)
793            // Actually, for external commands, custom FDs need to be inherited/set up.
794            // But we can update the shell's FD table temporarily if we want?
795            // Existing logic for custom FD WAS to update fd_table.
796            shell_state.fd_table.borrow_mut().open_fd(
797                fd,
798                &expanded_file,
799                false, // read
800                true,  // write
801                append,
802                !append, // truncate if not appending
803            )?;
804        }
805    } else {
806        // Current process redirection (builtins, command groups)
807        // We MUST update the file descriptor table for ALL FDs including 1 and 2
808        shell_state.fd_table.borrow_mut().open_fd(
809            fd,
810            &expanded_file,
811            false, // read
812            true,  // write
813            append,
814            !append, // truncate if not appending
815        )?;
816
817        // Also perform OS-level dup2 to ensure child processes inherit the redirection
818        // (This is critical for external commands running inside command groups)
819        let raw_fd = shell_state.fd_table.borrow().get_raw_fd(fd);
820        if let Some(rfd) = raw_fd {
821            // Avoid dup2-ing to itself if raw_fd happens to equal fd (unlikely but possible if we closed 1 then opened)
822            if rfd != fd {
823                unsafe {
824                    if libc::dup2(rfd, fd) < 0 {
825                        return Err(format!("Failed to dup2 fd {} to {}", rfd, fd));
826                    }
827                }
828            }
829        }
830    }
831
832    Ok(())
833}
834
835/// Apply file descriptor duplication
836fn apply_fd_duplication(
837    target_fd: i32,
838    source_fd: i32,
839    shell_state: &mut ShellState,
840    _command: Option<&mut Command>,
841) -> Result<(), String> {
842    // Check if source_fd is explicitly closed before attempting duplication
843    if shell_state.fd_table.borrow().is_closed(source_fd) {
844        let error_msg = format!("File descriptor {} is closed", source_fd);
845        if shell_state.colors_enabled {
846            eprintln!(
847                "{}Redirection error: {}\x1b[0m",
848                shell_state.color_scheme.error, error_msg
849            );
850        } else {
851            eprintln!("Redirection error: {}", error_msg);
852        }
853        return Err(error_msg);
854    }
855
856    // Duplicate source_fd to target_fd
857    shell_state
858        .fd_table
859        .borrow_mut()
860        .duplicate_fd(source_fd, target_fd)?;
861    Ok(())
862}
863
864/// Apply file descriptor closing
865fn apply_fd_close(
866    fd: i32,
867    shell_state: &mut ShellState,
868    command: Option<&mut Command>,
869) -> Result<(), String> {
870    // Close the specified fd in the fd table
871    shell_state.fd_table.borrow_mut().close_fd(fd)?;
872
873    // For external commands, we need to redirect the fd to /dev/null
874    // This ensures that writes to the closed fd don't produce errors
875    if let Some(cmd) = command {
876        match fd {
877            0 => {
878                // Close stdin - redirect to /dev/null for reading
879                cmd.stdin(Stdio::null());
880            }
881            1 => {
882                // Close stdout - redirect to /dev/null for writing
883                cmd.stdout(Stdio::null());
884            }
885            2 => {
886                // Close stderr - redirect to /dev/null for writing
887                cmd.stderr(Stdio::null());
888            }
889            _ => {
890                // For custom fds (3+), we use pre_exec to close them
891                // This is handled via the fd_table and dup2 operations
892            }
893        }
894    }
895
896    Ok(())
897}
898
899/// Apply read/write file descriptor opening
900fn apply_fd_input_output(
901    fd: i32,
902    file: &str,
903    shell_state: &mut ShellState,
904    _command: Option<&mut Command>,
905) -> Result<(), String> {
906    let expanded_file = expand_variables_in_string(file, shell_state);
907
908    // Open file for both reading and writing
909    shell_state.fd_table.borrow_mut().open_fd(
910        fd,
911        &expanded_file,
912        true,  // read
913        true,  // write
914        false, // append
915        false, // truncate
916    )?;
917
918    Ok(())
919}
920
921/// Apply here-document redirection
922fn apply_heredoc_redirection(
923    fd: i32,
924    delimiter: &str,
925    quoted: bool,
926    shell_state: &mut ShellState,
927    command: Option<&mut Command>,
928) -> Result<(), String> {
929    let here_doc_content = collect_here_document_content(delimiter, shell_state);
930
931    // Expand variables and command substitutions ONLY if delimiter was not quoted
932    let expanded_content = if quoted {
933        here_doc_content
934    } else {
935        expand_variables_in_string(&here_doc_content, shell_state)
936    };
937
938    // Create a pipe and write the content
939    let (reader, mut writer) =
940        pipe().map_err(|e| format!("Failed to create pipe for here-document: {}", e))?;
941
942    writeln!(writer, "{}", expanded_content)
943        .map_err(|e| format!("Failed to write here-document content: {}", e))?;
944
945    // Apply to stdin if fd is 0
946    if fd == 0 {
947        if let Some(cmd) = command {
948            cmd.stdin(Stdio::from(reader));
949        }
950    }
951
952    Ok(())
953}
954
955/// Apply here-string redirection
956fn apply_herestring_redirection(
957    fd: i32,
958    content: &str,
959    shell_state: &mut ShellState,
960    command: Option<&mut Command>,
961) -> Result<(), String> {
962    let expanded_content = expand_variables_in_string(content, shell_state);
963
964    // Create a pipe and write the content
965    let (reader, mut writer) =
966        pipe().map_err(|e| format!("Failed to create pipe for here-string: {}", e))?;
967
968    write!(writer, "{}", expanded_content)
969        .map_err(|e| format!("Failed to write here-string content: {}", e))?;
970
971    // Apply to stdin if fd is 0
972    if fd == 0 {
973        if let Some(cmd) = command {
974            cmd.stdin(Stdio::from(reader));
975        }
976    }
977
978    Ok(())
979}
980
981/// Execute a trap handler command
982/// Note: Signal masking during trap execution will be added in a future update
983pub fn execute_trap_handler(trap_cmd: &str, shell_state: &mut ShellState) -> i32 {
984    // Save current exit code to preserve it across trap execution
985    let saved_exit_code = shell_state.last_exit_code;
986
987    // TODO: Add signal masking to prevent recursive trap calls
988    // This requires careful handling of the nix sigprocmask API
989    // For now, traps execute without signal masking
990
991    // Parse and execute the trap command
992    let result = match crate::lexer::lex(trap_cmd, shell_state) {
993        Ok(tokens) => {
994            match crate::lexer::expand_aliases(
995                tokens,
996                shell_state,
997                &mut std::collections::HashSet::new(),
998            ) {
999                Ok(expanded_tokens) => {
1000                    match crate::parser::parse(expanded_tokens) {
1001                        Ok(ast) => execute(ast, shell_state),
1002                        Err(_) => {
1003                            // Parse error in trap handler - silently continue
1004                            saved_exit_code
1005                        }
1006                    }
1007                }
1008                Err(_) => {
1009                    // Alias expansion error - silently continue
1010                    saved_exit_code
1011                }
1012            }
1013        }
1014        Err(_) => {
1015            // Lex error in trap handler - silently continue
1016            saved_exit_code
1017        }
1018    };
1019
1020    // Restore the original exit code (trap handlers don't affect $?)
1021    shell_state.last_exit_code = saved_exit_code;
1022
1023    result
1024}
1025
1026pub fn execute(ast: Ast, shell_state: &mut ShellState) -> i32 {
1027    match ast {
1028        Ast::Assignment { var, value } => {
1029            // Expand variables and command substitutions in the value
1030            let expanded_value = expand_variables_in_string(&value, shell_state);
1031            shell_state.set_var(&var, expanded_value);
1032            0
1033        }
1034        Ast::LocalAssignment { var, value } => {
1035            // Expand variables and command substitutions in the value
1036            let expanded_value = expand_variables_in_string(&value, shell_state);
1037            shell_state.set_local_var(&var, expanded_value);
1038            0
1039        }
1040        Ast::Pipeline(commands) => {
1041            if commands.is_empty() {
1042                return 0;
1043            }
1044
1045            if commands.len() == 1 {
1046                // Single command, handle redirections
1047                execute_single_command(&commands[0], shell_state)
1048            } else {
1049                // Pipeline
1050                execute_pipeline(&commands, shell_state)
1051            }
1052        }
1053        Ast::Sequence(asts) => {
1054            let mut exit_code = 0;
1055            for ast in asts {
1056                exit_code = execute(ast, shell_state);
1057
1058                // Check if we got an early return from a function
1059                if shell_state.is_returning() {
1060                    return exit_code;
1061                }
1062
1063                // Check if exit was requested (e.g., from trap handler)
1064                if shell_state.exit_requested {
1065                    return shell_state.exit_code;
1066                }
1067            }
1068            exit_code
1069        }
1070        Ast::If {
1071            branches,
1072            else_branch,
1073        } => {
1074            for (condition, then_branch) in branches {
1075                let cond_exit = execute(*condition, shell_state);
1076                if cond_exit == 0 {
1077                    let exit_code = execute(*then_branch, shell_state);
1078
1079                    // Check if we got an early return from a function
1080                    if shell_state.is_returning() {
1081                        return exit_code;
1082                    }
1083
1084                    return exit_code;
1085                }
1086            }
1087            if let Some(else_b) = else_branch {
1088                let exit_code = execute(*else_b, shell_state);
1089
1090                // Check if we got an early return from a function
1091                if shell_state.is_returning() {
1092                    return exit_code;
1093                }
1094
1095                exit_code
1096            } else {
1097                0
1098            }
1099        }
1100        Ast::Case {
1101            word,
1102            cases,
1103            default,
1104        } => {
1105            for (patterns, branch) in cases {
1106                for pattern in &patterns {
1107                    if let Ok(glob_pattern) = glob::Pattern::new(pattern) {
1108                        if glob_pattern.matches(&word) {
1109                            let exit_code = execute(branch, shell_state);
1110
1111                            // Check if we got an early return from a function
1112                            if shell_state.is_returning() {
1113                                return exit_code;
1114                            }
1115
1116                            return exit_code;
1117                        }
1118                    } else {
1119                        // If pattern is invalid, fall back to exact match
1120                        if &word == pattern {
1121                            let exit_code = execute(branch, shell_state);
1122
1123                            // Check if we got an early return from a function
1124                            if shell_state.is_returning() {
1125                                return exit_code;
1126                            }
1127
1128                            return exit_code;
1129                        }
1130                    }
1131                }
1132            }
1133            if let Some(def) = default {
1134                let exit_code = execute(*def, shell_state);
1135
1136                // Check if we got an early return from a function
1137                if shell_state.is_returning() {
1138                    return exit_code;
1139                }
1140
1141                exit_code
1142            } else {
1143                0
1144            }
1145        }
1146        Ast::For {
1147            variable,
1148            items,
1149            body,
1150        } => {
1151            let mut exit_code = 0;
1152
1153            // Execute the loop body for each item
1154            for item in items {
1155                // Process any pending signals before executing the body
1156                crate::state::process_pending_signals(shell_state);
1157
1158                // Check if exit was requested (e.g., from trap handler)
1159                if shell_state.exit_requested {
1160                    return shell_state.exit_code;
1161                }
1162
1163                // Set the loop variable
1164                shell_state.set_var(&variable, item.clone());
1165
1166                // Execute the body
1167                exit_code = execute(*body.clone(), shell_state);
1168
1169                // Check if we got an early return from a function
1170                if shell_state.is_returning() {
1171                    return exit_code;
1172                }
1173
1174                // Check if exit was requested after executing the body
1175                if shell_state.exit_requested {
1176                    return shell_state.exit_code;
1177                }
1178            }
1179
1180            exit_code
1181        }
1182        Ast::While { condition, body } => {
1183            let mut exit_code = 0;
1184
1185            // Execute the loop while condition is true (exit code 0)
1186            loop {
1187                // Evaluate the condition
1188                let cond_exit = execute(*condition.clone(), shell_state);
1189
1190                // Check if we got an early return from a function
1191                if shell_state.is_returning() {
1192                    return cond_exit;
1193                }
1194
1195                // Check if exit was requested (e.g., from trap handler)
1196                if shell_state.exit_requested {
1197                    return shell_state.exit_code;
1198                }
1199
1200                // If condition is false (non-zero exit code), break
1201                if cond_exit != 0 {
1202                    break;
1203                }
1204
1205                // Execute the body
1206                exit_code = execute(*body.clone(), shell_state);
1207
1208                // Check if we got an early return from a function
1209                if shell_state.is_returning() {
1210                    return exit_code;
1211                }
1212
1213                // Check if exit was requested (e.g., from trap handler)
1214                if shell_state.exit_requested {
1215                    return shell_state.exit_code;
1216                }
1217            }
1218
1219            exit_code
1220        }
1221        Ast::FunctionDefinition { name, body } => {
1222            // Store function definition in shell state
1223            shell_state.define_function(name.clone(), *body);
1224            0
1225        }
1226        Ast::FunctionCall { name, args } => {
1227            if let Some(function_body) = shell_state.get_function(&name).cloned() {
1228                // Check recursion limit before entering function
1229                if shell_state.function_depth >= shell_state.max_recursion_depth {
1230                    eprintln!(
1231                        "Function recursion limit ({}) exceeded",
1232                        shell_state.max_recursion_depth
1233                    );
1234                    return 1;
1235                }
1236
1237                // Enter function context for local variable scoping
1238                shell_state.enter_function();
1239
1240                // Set up arguments as regular variables (will be enhanced in Phase 2)
1241                let old_positional = shell_state.positional_params.clone();
1242
1243                // Set positional parameters for function arguments
1244                shell_state.set_positional_params(args.clone());
1245
1246                // Execute function body
1247                let exit_code = execute(function_body, shell_state);
1248
1249                // Check if we got an early return from the function
1250                if shell_state.is_returning() {
1251                    let return_value = shell_state.get_return_value().unwrap_or(0);
1252
1253                    // Restore old positional parameters
1254                    shell_state.set_positional_params(old_positional);
1255
1256                    // Exit function context
1257                    shell_state.exit_function();
1258
1259                    // Clear return state
1260                    shell_state.clear_return();
1261
1262                    // Return the early return value
1263                    return return_value;
1264                }
1265
1266                // Restore old positional parameters
1267                shell_state.set_positional_params(old_positional);
1268
1269                // Exit function context
1270                shell_state.exit_function();
1271
1272                exit_code
1273            } else {
1274                eprintln!("Function '{}' not found", name);
1275                1
1276            }
1277        }
1278        Ast::Return { value } => {
1279            // Return statements can only be used inside functions
1280            if shell_state.function_depth == 0 {
1281                eprintln!("Return statement outside of function");
1282                return 1;
1283            }
1284
1285            // Parse return value if provided
1286            let exit_code = if let Some(ref val) = value {
1287                val.parse::<i32>().unwrap_or(0)
1288            } else {
1289                0
1290            };
1291
1292            // Set return state to indicate early return from function
1293            shell_state.set_return(exit_code);
1294
1295            // Return the exit code - the function call handler will check for this
1296            exit_code
1297        }
1298        Ast::And { left, right } => {
1299            // Execute left side first
1300            let left_exit = execute(*left, shell_state);
1301
1302            // Check if we got an early return from a function
1303            if shell_state.is_returning() {
1304                return left_exit;
1305            }
1306
1307            // Only execute right side if left succeeded (exit code 0)
1308            if left_exit == 0 {
1309                execute(*right, shell_state)
1310            } else {
1311                left_exit
1312            }
1313        }
1314        Ast::Or { left, right } => {
1315            // Execute left side first
1316            let left_exit = execute(*left, shell_state);
1317
1318            // Check if we got an early return from a function
1319            if shell_state.is_returning() {
1320                return left_exit;
1321            }
1322
1323            // Only execute right side if left failed (exit code != 0)
1324            if left_exit != 0 {
1325                execute(*right, shell_state)
1326            } else {
1327                left_exit
1328            }
1329        }
1330        Ast::Subshell { body } => execute_subshell(*body, shell_state),
1331        Ast::CommandGroup { body } => execute(*body, shell_state),
1332    }
1333}
1334
1335fn execute_single_command(cmd: &ShellCommand, shell_state: &mut ShellState) -> i32 {
1336    // Check if this is a compound command (subshell)
1337    if let Some(ref compound_ast) = cmd.compound {
1338        // Execute compound command with redirections
1339        return execute_compound_with_redirections(compound_ast, shell_state, &cmd.redirections);
1340    }
1341
1342    if cmd.args.is_empty() {
1343        // No command, but may have redirections - process them for side effects
1344        if !cmd.redirections.is_empty() {
1345            if let Err(e) = apply_redirections(&cmd.redirections, shell_state, None) {
1346                if shell_state.colors_enabled {
1347                    eprintln!(
1348                        "{}Redirection error: {}\x1b[0m",
1349                        shell_state.color_scheme.error, e
1350                    );
1351                } else {
1352                    eprintln!("Redirection error: {}", e);
1353                }
1354                return 1;
1355            }
1356        }
1357        return 0;
1358    }
1359
1360    // First expand variables, then wildcards
1361    let var_expanded_args = expand_variables_in_args(&cmd.args, shell_state);
1362    let expanded_args = match expand_wildcards(&var_expanded_args) {
1363        Ok(args) => args,
1364        Err(_) => return 1,
1365    };
1366
1367    if expanded_args.is_empty() {
1368        return 0;
1369    }
1370
1371    // Check if this is a function call
1372    if shell_state.get_function(&expanded_args[0]).is_some() {
1373        // This is a function call - create a FunctionCall AST node and execute it
1374        let function_call = Ast::FunctionCall {
1375            name: expanded_args[0].clone(),
1376            args: expanded_args[1..].to_vec(),
1377        };
1378        return execute(function_call, shell_state);
1379    }
1380
1381    if crate::builtins::is_builtin(&expanded_args[0]) {
1382        // Create a temporary ShellCommand with expanded args
1383        let temp_cmd = ShellCommand {
1384            args: expanded_args,
1385            redirections: cmd.redirections.clone(),
1386            compound: None,
1387        };
1388
1389        // If we're capturing output, create a writer for it
1390        if let Some(ref capture_buffer) = shell_state.capture_output.clone() {
1391            // Create a writer that writes to our capture buffer
1392            struct CaptureWriter {
1393                buffer: Rc<RefCell<Vec<u8>>>,
1394            }
1395            impl std::io::Write for CaptureWriter {
1396                fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1397                    self.buffer.borrow_mut().extend_from_slice(buf);
1398                    Ok(buf.len())
1399                }
1400                fn flush(&mut self) -> std::io::Result<()> {
1401                    Ok(())
1402                }
1403            }
1404            let writer = CaptureWriter {
1405                buffer: capture_buffer.clone(),
1406            };
1407            crate::builtins::execute_builtin(&temp_cmd, shell_state, Some(Box::new(writer)))
1408        } else {
1409            crate::builtins::execute_builtin(&temp_cmd, shell_state, None)
1410        }
1411    } else {
1412        // Separate environment variable assignments from the actual command
1413        // Environment vars must come before the command and have the form VAR=value
1414        let mut env_assignments = Vec::new();
1415        let mut command_start_idx = 0;
1416
1417        for (idx, arg) in expanded_args.iter().enumerate() {
1418            // Check if this looks like an environment variable assignment
1419            if let Some(eq_pos) = arg.find('=')
1420                && eq_pos > 0
1421            {
1422                let var_part = &arg[..eq_pos];
1423                // Check if var_part is a valid variable name
1424                if var_part
1425                    .chars()
1426                    .next()
1427                    .map(|c| c.is_alphabetic() || c == '_')
1428                    .unwrap_or(false)
1429                    && var_part.chars().all(|c| c.is_alphanumeric() || c == '_')
1430                {
1431                    env_assignments.push(arg.clone());
1432                    command_start_idx = idx + 1;
1433                    continue;
1434                }
1435            }
1436            // If we reach here, this is not an env assignment, so we've found the command
1437            break;
1438        }
1439
1440        // Check if we have a command to execute (vs just env assignments)
1441        let has_command = command_start_idx < expanded_args.len();
1442
1443        // If all args were env assignments, set them in the shell
1444        // but continue to process redirections per POSIX
1445        if !has_command {
1446            for assignment in &env_assignments {
1447                if let Some(eq_pos) = assignment.find('=') {
1448                    let var_name = &assignment[..eq_pos];
1449                    let var_value = &assignment[eq_pos + 1..];
1450                    shell_state.set_var(var_name, var_value.to_string());
1451                }
1452            }
1453
1454            // Process redirections even without a command
1455            if !cmd.redirections.is_empty() {
1456                if let Err(e) = apply_redirections(&cmd.redirections, shell_state, None) {
1457                    if shell_state.colors_enabled {
1458                        eprintln!(
1459                            "{}Redirection error: {}\x1b[0m",
1460                            shell_state.color_scheme.error, e
1461                        );
1462                    } else {
1463                        eprintln!("Redirection error: {}", e);
1464                    }
1465                    return 1;
1466                }
1467            }
1468            return 0;
1469        }
1470
1471        // Prepare command
1472        let mut command = Command::new(&expanded_args[command_start_idx]);
1473        command.args(&expanded_args[command_start_idx + 1..]);
1474
1475        // Check for stdin override (for pipeline subshells)
1476        if let Some(fd) = shell_state.stdin_override {
1477            unsafe {
1478                let dup_fd = libc::dup(fd);
1479                if dup_fd >= 0 {
1480                    command.stdin(Stdio::from_raw_fd(dup_fd));
1481                }
1482            }
1483        }
1484
1485        // Set environment for child process
1486        let mut child_env = shell_state.get_env_for_child();
1487
1488        // Add the per-command environment variable assignments
1489        for assignment in env_assignments {
1490            if let Some(eq_pos) = assignment.find('=') {
1491                let var_name = assignment[..eq_pos].to_string();
1492                let var_value = assignment[eq_pos + 1..].to_string();
1493                child_env.insert(var_name, var_value);
1494            }
1495        }
1496
1497        command.env_clear();
1498        for (key, value) in child_env {
1499            command.env(key, value);
1500        }
1501
1502        // If we're capturing output, redirect stdout to capture buffer
1503        let capturing = shell_state.capture_output.is_some();
1504        if capturing {
1505            command.stdout(Stdio::piped());
1506        }
1507
1508        // Apply all redirections
1509        if let Err(e) = apply_redirections(&cmd.redirections, shell_state, Some(&mut command)) {
1510            if shell_state.colors_enabled {
1511                eprintln!(
1512                    "{}Redirection error: {}\x1b[0m",
1513                    shell_state.color_scheme.error, e
1514                );
1515            } else {
1516                eprintln!("Redirection error: {}", e);
1517            }
1518            return 1;
1519        }
1520
1521        // Apply custom file descriptors (3-9) from fd table to external command
1522        // We need to keep the FD table borrowed until after the child is spawned
1523        // to prevent File handles from being dropped and FDs from being closed
1524        let custom_fds: Vec<(i32, RawFd)> = {
1525            let fd_table = shell_state.fd_table.borrow();
1526            let mut fds = Vec::new();
1527
1528            for fd_num in 3..=9 {
1529                if fd_table.is_open(fd_num) {
1530                    if let Some(raw_fd) = fd_table.get_raw_fd(fd_num) {
1531                        fds.push((fd_num, raw_fd));
1532                    }
1533                }
1534            }
1535
1536            fds
1537        };
1538
1539        // If we have custom fds to apply, use pre_exec to set them in the child
1540        if !custom_fds.is_empty() {
1541            unsafe {
1542                command.pre_exec(move || {
1543                    for (target_fd, source_fd) in &custom_fds {
1544                        let result = libc::dup2(*source_fd, *target_fd);
1545                        if result < 0 {
1546                            return Err(std::io::Error::last_os_error());
1547                        }
1548                    }
1549                    Ok(())
1550                });
1551            }
1552        }
1553
1554        // Spawn and execute the command
1555        // Note: The FD table borrow above has been released, but the custom_fds
1556        // closure capture keeps the file handles alive
1557        match command.spawn() {
1558            Ok(mut child) => {
1559                // If capturing, read stdout
1560                if capturing {
1561                    if let Some(mut stdout) = child.stdout.take() {
1562                        use std::io::Read;
1563                        let mut output = Vec::new();
1564                        if stdout.read_to_end(&mut output).is_ok() {
1565                            if let Some(ref capture_buffer) = shell_state.capture_output {
1566                                capture_buffer.borrow_mut().extend_from_slice(&output);
1567                            }
1568                        }
1569                    }
1570                }
1571
1572                match child.wait() {
1573                    Ok(status) => status.code().unwrap_or(0),
1574                    Err(e) => {
1575                        if shell_state.colors_enabled {
1576                            eprintln!(
1577                                "{}Error waiting for command: {}\x1b[0m",
1578                                shell_state.color_scheme.error, e
1579                            );
1580                        } else {
1581                            eprintln!("Error waiting for command: {}", e);
1582                        }
1583                        1
1584                    }
1585                }
1586            }
1587            Err(e) => {
1588                if shell_state.colors_enabled {
1589                    eprintln!(
1590                        "{}Command spawn error: {}\x1b[0m",
1591                        shell_state.color_scheme.error, e
1592                    );
1593                } else {
1594                    eprintln!("Command spawn error: {}", e);
1595                }
1596                1
1597            }
1598        }
1599    }
1600}
1601
1602fn execute_pipeline(commands: &[ShellCommand], shell_state: &mut ShellState) -> i32 {
1603    let mut exit_code = 0;
1604    let mut previous_stdout: Option<File> = None;
1605
1606    for (i, cmd) in commands.iter().enumerate() {
1607        let is_last = i == commands.len() - 1;
1608
1609        if let Some(ref compound_ast) = cmd.compound {
1610            // Execute compound command (subshell) in pipeline
1611            let (com_exit_code, com_stdout) = execute_compound_in_pipeline(
1612                compound_ast,
1613                shell_state,
1614                previous_stdout.take(),
1615                i == 0,
1616                is_last,
1617                &cmd.redirections,
1618            );
1619            exit_code = com_exit_code;
1620            previous_stdout = com_stdout;
1621            continue;
1622        }
1623
1624        if cmd.args.is_empty() {
1625            continue;
1626        }
1627
1628        // First expand variables, then wildcards
1629        let var_expanded_args = expand_variables_in_args(&cmd.args, shell_state);
1630        let expanded_args = match expand_wildcards(&var_expanded_args) {
1631            Ok(args) => args,
1632            Err(_) => return 1,
1633        };
1634
1635        if expanded_args.is_empty() {
1636            continue;
1637        }
1638
1639        if crate::builtins::is_builtin(&expanded_args[0]) {
1640            // Built-ins in pipelines are tricky - for now, execute them separately
1641            // This is not perfect but better than nothing
1642            let temp_cmd = ShellCommand {
1643                args: expanded_args,
1644                redirections: cmd.redirections.clone(),
1645                compound: None,
1646            };
1647            if !is_last {
1648                // Create a safe pipe
1649                let (reader, writer) = match pipe() {
1650                    Ok((r, w)) => (unsafe { File::from_raw_fd(r.into_raw_fd()) }, w),
1651                    Err(e) => {
1652                        if shell_state.colors_enabled {
1653                            eprintln!(
1654                                "{}Error creating pipe for builtin: {}\x1b[0m",
1655                                shell_state.color_scheme.error, e
1656                            );
1657                        } else {
1658                            eprintln!("Error creating pipe for builtin: {}", e);
1659                        }
1660                        return 1;
1661                    }
1662                };
1663                // Execute builtin with writer for output capture
1664                exit_code = crate::builtins::execute_builtin(
1665                    &temp_cmd,
1666                    shell_state,
1667                    Some(Box::new(writer)),
1668                );
1669                // Use reader for next command's stdin
1670                previous_stdout = Some(reader);
1671            } else {
1672                // Last command: check if we're capturing output
1673                if let Some(ref capture_buffer) = shell_state.capture_output.clone() {
1674                    // Create a writer that writes to our capture buffer
1675                    struct CaptureWriter {
1676                        buffer: Rc<RefCell<Vec<u8>>>,
1677                    }
1678                    impl std::io::Write for CaptureWriter {
1679                        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1680                            self.buffer.borrow_mut().extend_from_slice(buf);
1681                            Ok(buf.len())
1682                        }
1683                        fn flush(&mut self) -> std::io::Result<()> {
1684                            Ok(())
1685                        }
1686                    }
1687                    let writer = CaptureWriter {
1688                        buffer: capture_buffer.clone(),
1689                    };
1690                    exit_code = crate::builtins::execute_builtin(
1691                        &temp_cmd,
1692                        shell_state,
1693                        Some(Box::new(writer)),
1694                    );
1695                } else {
1696                    // Not capturing, execute normally
1697                    exit_code = crate::builtins::execute_builtin(&temp_cmd, shell_state, None);
1698                }
1699                previous_stdout = None;
1700            }
1701        } else {
1702            let mut command = Command::new(&expanded_args[0]);
1703            command.args(&expanded_args[1..]);
1704
1705            // Set environment for child process
1706            let child_env = shell_state.get_env_for_child();
1707            command.env_clear();
1708            for (key, value) in child_env {
1709                command.env(key, value);
1710            }
1711
1712            // Set stdin from previous command's stdout
1713            if let Some(prev) = previous_stdout.take() {
1714                command.stdin(Stdio::from(prev));
1715            } else if i > 0 {
1716                // We are in a pipeline (not first command) but have no input pipe.
1717                // This means the previous command didn't produce a pipe.
1718                // We should treat this as empty input (EOF), not inherit stdin!
1719                command.stdin(Stdio::null());
1720            } else if let Some(fd) = shell_state.stdin_override {
1721                // We have a stdin override (e.g. from parent subshell)
1722                // We must duplicate it because Stdio takes ownership
1723                unsafe {
1724                    let dup_fd = libc::dup(fd);
1725                    if dup_fd >= 0 {
1726                        command.stdin(Stdio::from_raw_fd(dup_fd));
1727                    }
1728                }
1729            }
1730
1731            // Set stdout for next command, or for capturing if this is the last
1732            if !is_last {
1733                command.stdout(Stdio::piped());
1734            } else if shell_state.capture_output.is_some() {
1735                // Last command in pipeline but we're capturing output
1736                command.stdout(Stdio::piped());
1737            }
1738
1739            // Apply redirections for this command
1740            if let Err(e) = apply_redirections(&cmd.redirections, shell_state, Some(&mut command)) {
1741                if shell_state.colors_enabled {
1742                    eprintln!(
1743                        "{}Redirection error: {}\x1b[0m",
1744                        shell_state.color_scheme.error, e
1745                    );
1746                } else {
1747                    eprintln!("Redirection error: {}", e);
1748                }
1749                return 1;
1750            }
1751
1752            match command.spawn() {
1753                Ok(mut child) => {
1754                    if !is_last {
1755                        previous_stdout = child
1756                            .stdout
1757                            .take()
1758                            .map(|s| unsafe { File::from_raw_fd(s.into_raw_fd()) });
1759                    } else if shell_state.capture_output.is_some() {
1760                        // Last command and we're capturing - read its output
1761                        if let Some(mut stdout) = child.stdout.take() {
1762                            use std::io::Read;
1763                            let mut output = Vec::new();
1764                            if stdout.read_to_end(&mut output).is_ok()
1765                                && let Some(ref capture_buffer) = shell_state.capture_output
1766                            {
1767                                capture_buffer.borrow_mut().extend_from_slice(&output);
1768                            }
1769                        }
1770                    }
1771                    match child.wait() {
1772                        Ok(status) => {
1773                            exit_code = status.code().unwrap_or(0);
1774                        }
1775                        Err(e) => {
1776                            if shell_state.colors_enabled {
1777                                eprintln!(
1778                                    "{}Error waiting for command: {}\x1b[0m",
1779                                    shell_state.color_scheme.error, e
1780                                );
1781                            } else {
1782                                eprintln!("Error waiting for command: {}", e);
1783                            }
1784                            exit_code = 1;
1785                        }
1786                    }
1787                }
1788                Err(e) => {
1789                    if shell_state.colors_enabled {
1790                        eprintln!(
1791                            "{}Error spawning command '{}{}",
1792                            shell_state.color_scheme.error,
1793                            expanded_args[0],
1794                            &format!("': {}\x1b[0m", e)
1795                        );
1796                    } else {
1797                        eprintln!("Error spawning command '{}': {}", expanded_args[0], e);
1798                    }
1799                    exit_code = 1;
1800                }
1801            }
1802        }
1803    }
1804
1805    exit_code
1806}
1807
1808/// Execute a subshell with isolated state
1809///
1810/// # Arguments
1811/// * `body` - The AST to execute in the subshell
1812/// * `shell_state` - The parent shell state (will be cloned)
1813///
1814/// # Returns
1815/// * Exit code from the subshell execution
1816///
1817/// # Behavior
1818/// - Clones the shell state for isolation
1819/// - Executes the body in the cloned state
1820/// - Returns the exit code without modifying parent state
1821/// - Preserves parent state completely (variables, functions, etc.)
1822/// - Tracks subshell depth to prevent stack overflow
1823/// - Handles exit and return commands properly (isolated from parent)
1824/// - Cleans up file descriptors to prevent resource leaks
1825fn execute_subshell(body: Ast, shell_state: &mut ShellState) -> i32 {
1826    // Check depth limit to prevent stack overflow
1827    if shell_state.subshell_depth >= MAX_SUBSHELL_DEPTH {
1828        if shell_state.colors_enabled {
1829            eprintln!(
1830                "{}Subshell nesting limit ({}) exceeded\x1b[0m",
1831                shell_state.color_scheme.error, MAX_SUBSHELL_DEPTH
1832            );
1833        } else {
1834            eprintln!("Subshell nesting limit ({}) exceeded", MAX_SUBSHELL_DEPTH);
1835        }
1836        shell_state.last_exit_code = 1;
1837        return 1;
1838    }
1839
1840    // Save current directory for restoration
1841    let original_dir = std::env::current_dir().ok();
1842
1843    // Clone the shell state for isolation
1844    let mut subshell_state = shell_state.clone();
1845
1846    // Deep clone the file descriptor table for isolation
1847    // shell_state.clone() only clones the Rc, so we need to manually deep clone the table
1848    // and put it in a new Rc<RefCell<_>>
1849    match shell_state.fd_table.borrow().deep_clone() {
1850        Ok(new_fd_table) => {
1851            subshell_state.fd_table = Rc::new(RefCell::new(new_fd_table));
1852        }
1853        Err(e) => {
1854            if shell_state.colors_enabled {
1855                eprintln!(
1856                    "{}Failed to clone file descriptor table: {}\x1b[0m",
1857                    shell_state.color_scheme.error, e
1858                );
1859            } else {
1860                eprintln!("Failed to clone file descriptor table: {}", e);
1861            }
1862            return 1;
1863        }
1864    }
1865
1866    // Increment subshell depth in the cloned state
1867    subshell_state.subshell_depth = shell_state.subshell_depth + 1;
1868
1869    // Clone trap handlers for isolation (subshells inherit but don't affect parent)
1870    let parent_traps = shell_state.trap_handlers.lock().unwrap().clone();
1871    subshell_state.trap_handlers = std::sync::Arc::new(std::sync::Mutex::new(parent_traps));
1872
1873    // Execute the body in the isolated state
1874    let exit_code = execute(body, &mut subshell_state);
1875
1876    // Handle exit in subshell: exit should only exit the subshell, not the parent
1877    // The exit_requested flag is isolated to the subshell_state, so it won't affect parent
1878    let final_exit_code = if subshell_state.exit_requested {
1879        // Subshell called exit - use its exit code
1880        subshell_state.exit_code
1881    } else if subshell_state.is_returning() {
1882        // Subshell called return - treat as exit from subshell
1883        // Return in subshell should not propagate to parent function
1884        subshell_state.get_return_value().unwrap_or(exit_code)
1885    } else {
1886        exit_code
1887    };
1888
1889    // Clean up the subshell's file descriptor table to prevent resource leaks
1890    // This ensures any file descriptors opened in the subshell are properly released
1891    subshell_state.fd_table.borrow_mut().clear();
1892
1893    // Restore original directory (in case subshell changed it)
1894    if let Some(dir) = original_dir {
1895        let _ = std::env::set_current_dir(dir);
1896    }
1897
1898    // Update parent's last_exit_code to reflect subshell result
1899    shell_state.last_exit_code = final_exit_code;
1900
1901    // Return the exit code
1902    final_exit_code
1903}
1904
1905/// Execute a compound command with redirections
1906///
1907/// # Arguments
1908/// * `compound_ast` - The compound command AST
1909/// * `shell_state` - The shell state
1910/// * `redirections` - Redirections to apply
1911///
1912/// # Returns
1913/// * Exit code from the compound command
1914fn execute_compound_with_redirections(
1915    compound_ast: &Ast,
1916    shell_state: &mut ShellState,
1917    redirections: &[Redirection],
1918) -> i32 {
1919    match compound_ast {
1920        Ast::CommandGroup { body } => {
1921            // Save FDs before applying redirections
1922            if let Err(e) = shell_state.fd_table.borrow_mut().save_all_fds() {
1923                eprintln!("Error saving FDs: {}", e);
1924                return 1;
1925            }
1926
1927            // Apply redirections to current process
1928            if let Err(e) = apply_redirections(redirections, shell_state, None) {
1929                if shell_state.colors_enabled {
1930                    eprintln!("{}{}\u{001b}[0m", shell_state.color_scheme.error, e);
1931                } else {
1932                    eprintln!("{}", e);
1933                }
1934                shell_state.fd_table.borrow_mut().restore_all_fds().ok();
1935                return 1;
1936            }
1937
1938            // Execute the group body
1939            let exit_code = execute(*body.clone(), shell_state);
1940
1941            // Restore FDs
1942            if let Err(e) = shell_state.fd_table.borrow_mut().restore_all_fds() {
1943                eprintln!("Error restoring FDs: {}", e);
1944            }
1945
1946            exit_code
1947        }
1948        Ast::Subshell { body } => {
1949            // For subshells with redirections, we need to:
1950            // 1. Set up output capture if there are output redirections
1951            // 2. Execute the subshell
1952            // 3. Apply the redirections to the captured output
1953
1954            // Check if we have output redirections
1955            let has_output_redir = redirections.iter().any(|r| {
1956                matches!(
1957                    r,
1958                    Redirection::Output(_)
1959                        | Redirection::Append(_)
1960                        | Redirection::FdOutput(_, _)
1961                        | Redirection::FdAppend(_, _)
1962                )
1963            });
1964
1965            if has_output_redir {
1966                // Clone state for subshell
1967                let mut subshell_state = shell_state.clone();
1968
1969                // Set up output capture
1970                let capture_buffer = Rc::new(RefCell::new(Vec::new()));
1971                subshell_state.capture_output = Some(capture_buffer.clone());
1972
1973                // Execute subshell
1974                let exit_code = execute(*body.clone(), &mut subshell_state);
1975
1976                // Get captured output
1977                let output = capture_buffer.borrow().clone();
1978
1979                // Apply redirections to output
1980                for redir in redirections {
1981                    match redir {
1982                        Redirection::Output(file) => {
1983                            let expanded_file = expand_variables_in_string(file, shell_state);
1984                            if let Err(e) = std::fs::write(&expanded_file, &output) {
1985                                if shell_state.colors_enabled {
1986                                    eprintln!(
1987                                        "{}Redirection error: {}\x1b[0m",
1988                                        shell_state.color_scheme.error, e
1989                                    );
1990                                } else {
1991                                    eprintln!("Redirection error: {}", e);
1992                                }
1993                                return 1;
1994                            }
1995                        }
1996                        Redirection::Append(file) => {
1997                            let expanded_file = expand_variables_in_string(file, shell_state);
1998                            use std::fs::OpenOptions;
1999                            let mut file_handle = match OpenOptions::new()
2000                                .append(true)
2001                                .create(true)
2002                                .open(&expanded_file)
2003                            {
2004                                Ok(f) => f,
2005                                Err(e) => {
2006                                    if shell_state.colors_enabled {
2007                                        eprintln!(
2008                                            "{}Redirection error: {}\x1b[0m",
2009                                            shell_state.color_scheme.error, e
2010                                        );
2011                                    } else {
2012                                        eprintln!("Redirection error: {}", e);
2013                                    }
2014                                    return 1;
2015                                }
2016                            };
2017                            if let Err(e) = file_handle.write_all(&output) {
2018                                if shell_state.colors_enabled {
2019                                    eprintln!(
2020                                        "{}Redirection error: {}\x1b[0m",
2021                                        shell_state.color_scheme.error, e
2022                                    );
2023                                } else {
2024                                    eprintln!("Redirection error: {}", e);
2025                                }
2026                                return 1;
2027                            }
2028                        }
2029                        _ => {
2030                            // For Phase 2, only support basic output redirections
2031                            // Other redirections are silently ignored for subshells
2032                        }
2033                    }
2034                }
2035
2036                shell_state.last_exit_code = exit_code;
2037                exit_code
2038            } else {
2039                // No output redirections, execute normally
2040                execute_subshell(*body.clone(), shell_state)
2041            }
2042        }
2043        _ => {
2044            eprintln!("Unsupported compound command type");
2045            1
2046        }
2047    }
2048}
2049
2050/// Check if redirections include stdout redirections
2051/// Returns true if any redirection affects stdout (FD 1)
2052fn has_stdout_redirection(redirections: &[Redirection]) -> bool {
2053    redirections.iter().any(|r| match r {
2054        // Default output redirections affect stdout (FD 1)
2055        Redirection::Output(_) | Redirection::Append(_) => true,
2056        // Explicit FD 1 redirections
2057        Redirection::FdOutput(1, _) | Redirection::FdAppend(1, _) => true,
2058        // FD 1 duplication or closure
2059        Redirection::FdDuplicate(1, _) | Redirection::FdClose(1) => true,
2060        // All other redirections don't affect stdout
2061        _ => false,
2062    })
2063}
2064
2065/// Execute a compound command (subshell) as part of a pipeline
2066///
2067/// # Arguments
2068/// * `compound_ast` - The compound command AST (typically Subshell)
2069/// * `shell_state` - The parent shell state
2070/// * `is_last` - Whether this is the last command in the pipeline
2071/// * `redirections` - Redirections to apply to the compound command
2072///
2073/// # Returns
2074/// * Exit code from the compound command
2075fn execute_compound_in_pipeline(
2076    compound_ast: &Ast,
2077    shell_state: &mut ShellState,
2078    stdin: Option<File>,
2079    is_first: bool,
2080    is_last: bool,
2081    redirections: &[Redirection],
2082) -> (i32, Option<File>) {
2083    match compound_ast {
2084        Ast::Subshell { body } | Ast::CommandGroup { body } => {
2085            // Clone state for subshell
2086            let mut subshell_state = shell_state.clone();
2087
2088            // Setup stdin from provided file if available
2089            // We must keep the file alive for the duration of the subshell execution.
2090            let mut _stdin_file = stdin;
2091
2092            if let Some(ref f) = _stdin_file {
2093                let fd = f.as_raw_fd();
2094                subshell_state.stdin_override = Some(fd);
2095            } else if !is_first && subshell_state.stdin_override.is_none() {
2096                // If we have no input from previous stage and no override, use /dev/null
2097                if let Ok(f) = File::open("/dev/null") {
2098                    subshell_state.stdin_override = Some(f.as_raw_fd());
2099                    _stdin_file = Some(f);
2100                }
2101            }
2102
2103            // Setup output capture if not last or if parent is capturing
2104            // BUT skip capture if stdout is redirected (e.g., { pwd; } > out | wc -l)
2105            let capture_buffer = if (!is_last || shell_state.capture_output.is_some())
2106                && !has_stdout_redirection(redirections)
2107            {
2108                let buffer = Rc::new(RefCell::new(Vec::new()));
2109                subshell_state.capture_output = Some(buffer.clone());
2110                Some(buffer)
2111            } else {
2112                None
2113            };
2114
2115            // Apply redirections (saving/restoring if it's a group)
2116            let exit_code = if matches!(compound_ast, Ast::CommandGroup { .. }) {
2117                // Save FDs before applying redirections
2118                if let Err(e) = subshell_state.fd_table.borrow_mut().save_all_fds() {
2119                    eprintln!("Error saving FDs: {}", e);
2120                    return (1, None);
2121                }
2122
2123                // If we have a pipe from previous stage, hook it up to FD 0 for builtins
2124                if let Some(ref f) = _stdin_file {
2125                    unsafe {
2126                        libc::dup2(f.as_raw_fd(), 0);
2127                    }
2128                }
2129
2130                // Apply redirections to current process
2131                if let Err(e) = apply_redirections(redirections, &mut subshell_state, None) {
2132                    if subshell_state.colors_enabled {
2133                        eprintln!("{}{}\u{001b}[0m", subshell_state.color_scheme.error, e);
2134                    } else {
2135                        eprintln!("{}", e);
2136                    }
2137                    subshell_state.fd_table.borrow_mut().restore_all_fds().ok();
2138                    return (1, None);
2139                }
2140
2141                // Execute the body
2142                let code = execute(*body.clone(), &mut subshell_state);
2143
2144                // Restore FDs
2145                if let Err(e) = subshell_state.fd_table.borrow_mut().restore_all_fds() {
2146                    eprintln!("Error restoring FDs: {}", e);
2147                }
2148                code
2149            } else {
2150                // Subshell handling (non-forking)
2151                if let Err(e) = subshell_state.fd_table.borrow_mut().save_all_fds() {
2152                    eprintln!("Error saving FDs: {}", e);
2153                    return (1, None);
2154                }
2155
2156                // If we have a pipe from previous stage, hook it up to FD 0
2157                if let Some(ref f) = _stdin_file {
2158                    unsafe {
2159                        libc::dup2(f.as_raw_fd(), 0);
2160                    }
2161                }
2162
2163                if let Err(e) = apply_redirections(redirections, &mut subshell_state, None) {
2164                    eprintln!("{}", e);
2165                    subshell_state.fd_table.borrow_mut().restore_all_fds().ok();
2166                    return (1, None);
2167                }
2168                let code = execute(*body.clone(), &mut subshell_state);
2169                subshell_state.fd_table.borrow_mut().restore_all_fds().ok();
2170                code
2171            };
2172
2173            // Prepare stdout for next stage if captured
2174            let mut next_stdout = None;
2175            if let Some(buffer) = capture_buffer {
2176                let captured = buffer.borrow().clone();
2177
2178                // If not last, create a pipe and write captured output to it
2179                if !is_last {
2180                    use std::io::Write;
2181                    let (reader, mut writer) = match pipe() {
2182                        Ok((r, w)) => (r, w),
2183                        Err(e) => {
2184                            eprintln!("Error creating pipe for compound command: {}", e);
2185                            return (exit_code, None);
2186                        }
2187                    };
2188                    if let Err(e) = writer.write_all(&captured) {
2189                        eprintln!("Error writing to pipe: {}", e);
2190                    }
2191                    drop(writer); // Close write end so reader sees EOF
2192
2193                    next_stdout = Some(unsafe { File::from_raw_fd(reader.into_raw_fd()) });
2194                }
2195
2196                // If parent is capturing, also pass data up
2197                if let Some(ref parent_capture) = shell_state.capture_output {
2198                    parent_capture.borrow_mut().extend_from_slice(&captured);
2199                }
2200            }
2201
2202            shell_state.last_exit_code = exit_code;
2203            (exit_code, next_stdout)
2204        }
2205        _ => {
2206            eprintln!("Unsupported compound command in pipeline");
2207            (1, None)
2208        }
2209    }
2210}
2211
2212#[cfg(test)]
2213mod tests {
2214    use super::*;
2215    use std::sync::Mutex;
2216
2217    // Mutex to serialize tests that modify environment variables or create files
2218    static ENV_LOCK: Mutex<()> = Mutex::new(());
2219
2220    #[test]
2221    fn test_execute_single_command_builtin() {
2222        let cmd = ShellCommand {
2223            args: vec!["true".to_string()],
2224            redirections: Vec::new(),
2225            compound: None,
2226        };
2227        let mut shell_state = ShellState::new();
2228        let exit_code = execute_single_command(&cmd, &mut shell_state);
2229        assert_eq!(exit_code, 0);
2230    }
2231
2232    // For external commands, test with a command that exists
2233    #[test]
2234    fn test_execute_single_command_external() {
2235        let cmd = ShellCommand {
2236            args: vec!["true".to_string()], // Assume true exists
2237            redirections: Vec::new(),
2238            compound: None,
2239        };
2240        let mut shell_state = ShellState::new();
2241        let exit_code = execute_single_command(&cmd, &mut shell_state);
2242        assert_eq!(exit_code, 0);
2243    }
2244
2245    #[test]
2246    fn test_execute_single_command_external_nonexistent() {
2247        let cmd = ShellCommand {
2248            args: vec!["nonexistent_command".to_string()],
2249            redirections: Vec::new(),
2250            compound: None,
2251        };
2252        let mut shell_state = ShellState::new();
2253        let exit_code = execute_single_command(&cmd, &mut shell_state);
2254        assert_eq!(exit_code, 1); // Command not found
2255    }
2256
2257    #[test]
2258    fn test_execute_pipeline() {
2259        let commands = vec![
2260            ShellCommand {
2261                args: vec!["printf".to_string(), "hello".to_string()],
2262                redirections: Vec::new(),
2263                compound: None,
2264            },
2265            ShellCommand {
2266                args: vec!["cat".to_string()], // cat reads from stdin
2267                redirections: Vec::new(),
2268                compound: None,
2269            },
2270        ];
2271        let mut shell_state = ShellState::new();
2272        let exit_code = execute_pipeline(&commands, &mut shell_state);
2273        assert_eq!(exit_code, 0);
2274    }
2275
2276    #[test]
2277    fn test_execute_empty_pipeline() {
2278        let commands = vec![];
2279        let mut shell_state = ShellState::new();
2280        let exit_code = execute(Ast::Pipeline(commands), &mut shell_state);
2281        assert_eq!(exit_code, 0);
2282    }
2283
2284    #[test]
2285    fn test_execute_single_command() {
2286        let ast = Ast::Pipeline(vec![ShellCommand {
2287            args: vec!["true".to_string()],
2288            redirections: Vec::new(),
2289            compound: None,
2290        }]);
2291        let mut shell_state = ShellState::new();
2292        let exit_code = execute(ast, &mut shell_state);
2293        assert_eq!(exit_code, 0);
2294    }
2295
2296    #[test]
2297    fn test_execute_function_definition() {
2298        let ast = Ast::FunctionDefinition {
2299            name: "test_func".to_string(),
2300            body: Box::new(Ast::Pipeline(vec![ShellCommand {
2301                args: vec!["echo".to_string(), "hello".to_string()],
2302                redirections: Vec::new(),
2303                compound: None,
2304            }])),
2305        };
2306        let mut shell_state = ShellState::new();
2307        let exit_code = execute(ast, &mut shell_state);
2308        assert_eq!(exit_code, 0);
2309
2310        // Check that function was stored
2311        assert!(shell_state.get_function("test_func").is_some());
2312    }
2313
2314    #[test]
2315    fn test_execute_function_call() {
2316        // First define a function
2317        let mut shell_state = ShellState::new();
2318        shell_state.define_function(
2319            "test_func".to_string(),
2320            Ast::Pipeline(vec![ShellCommand {
2321                args: vec!["echo".to_string(), "hello".to_string()],
2322                redirections: Vec::new(),
2323                compound: None,
2324            }]),
2325        );
2326
2327        // Now call the function
2328        let ast = Ast::FunctionCall {
2329            name: "test_func".to_string(),
2330            args: vec![],
2331        };
2332        let exit_code = execute(ast, &mut shell_state);
2333        assert_eq!(exit_code, 0);
2334    }
2335
2336    #[test]
2337    fn test_execute_function_call_with_args() {
2338        // First define a function that uses arguments
2339        let mut shell_state = ShellState::new();
2340        shell_state.define_function(
2341            "test_func".to_string(),
2342            Ast::Pipeline(vec![ShellCommand {
2343                args: vec!["echo".to_string(), "arg1".to_string()],
2344                redirections: Vec::new(),
2345                compound: None,
2346            }]),
2347        );
2348
2349        // Now call the function with arguments
2350        let ast = Ast::FunctionCall {
2351            name: "test_func".to_string(),
2352            args: vec!["hello".to_string()],
2353        };
2354        let exit_code = execute(ast, &mut shell_state);
2355        assert_eq!(exit_code, 0);
2356    }
2357
2358    #[test]
2359    fn test_execute_nonexistent_function() {
2360        let mut shell_state = ShellState::new();
2361        let ast = Ast::FunctionCall {
2362            name: "nonexistent".to_string(),
2363            args: vec![],
2364        };
2365        let exit_code = execute(ast, &mut shell_state);
2366        assert_eq!(exit_code, 1); // Should return error code
2367    }
2368
2369    #[test]
2370    fn test_execute_function_integration() {
2371        // Test full integration: define function, then call it
2372        let mut shell_state = ShellState::new();
2373
2374        // First define a function
2375        let define_ast = Ast::FunctionDefinition {
2376            name: "hello".to_string(),
2377            body: Box::new(Ast::Pipeline(vec![ShellCommand {
2378                args: vec!["printf".to_string(), "Hello from function".to_string()],
2379                redirections: Vec::new(),
2380                compound: None,
2381            }])),
2382        };
2383        let exit_code = execute(define_ast, &mut shell_state);
2384        assert_eq!(exit_code, 0);
2385
2386        // Now call the function
2387        let call_ast = Ast::FunctionCall {
2388            name: "hello".to_string(),
2389            args: vec![],
2390        };
2391        let exit_code = execute(call_ast, &mut shell_state);
2392        assert_eq!(exit_code, 0);
2393    }
2394
2395    #[test]
2396    fn test_execute_function_with_local_variables() {
2397        let mut shell_state = ShellState::new();
2398
2399        // Set a global variable
2400        shell_state.set_var("global_var", "global_value".to_string());
2401
2402        // Define a function that uses local variables
2403        let define_ast = Ast::FunctionDefinition {
2404            name: "test_func".to_string(),
2405            body: Box::new(Ast::Sequence(vec![
2406                Ast::LocalAssignment {
2407                    var: "local_var".to_string(),
2408                    value: "local_value".to_string(),
2409                },
2410                Ast::Assignment {
2411                    var: "global_var".to_string(),
2412                    value: "modified_in_function".to_string(),
2413                },
2414                Ast::Pipeline(vec![ShellCommand {
2415                    args: vec!["printf".to_string(), "success".to_string()],
2416                    redirections: Vec::new(),
2417                    compound: None,
2418                }]),
2419            ])),
2420        };
2421        let exit_code = execute(define_ast, &mut shell_state);
2422        assert_eq!(exit_code, 0);
2423
2424        // Global variable should not be modified during function definition
2425        assert_eq!(
2426            shell_state.get_var("global_var"),
2427            Some("global_value".to_string())
2428        );
2429
2430        // Call the function
2431        let call_ast = Ast::FunctionCall {
2432            name: "test_func".to_string(),
2433            args: vec![],
2434        };
2435        let exit_code = execute(call_ast, &mut shell_state);
2436        assert_eq!(exit_code, 0);
2437
2438        // After function call, global variable should be modified since function assignments affect global scope
2439        assert_eq!(
2440            shell_state.get_var("global_var"),
2441            Some("modified_in_function".to_string())
2442        );
2443    }
2444
2445    #[test]
2446    fn test_execute_nested_function_calls() {
2447        let mut shell_state = ShellState::new();
2448
2449        // Set global variable
2450        shell_state.set_var("global_var", "global".to_string());
2451
2452        // Define outer function
2453        let outer_func = Ast::FunctionDefinition {
2454            name: "outer".to_string(),
2455            body: Box::new(Ast::Sequence(vec![
2456                Ast::Assignment {
2457                    var: "global_var".to_string(),
2458                    value: "outer_modified".to_string(),
2459                },
2460                Ast::FunctionCall {
2461                    name: "inner".to_string(),
2462                    args: vec![],
2463                },
2464                Ast::Pipeline(vec![ShellCommand {
2465                    args: vec!["printf".to_string(), "outer_done".to_string()],
2466                    redirections: Vec::new(),
2467                    compound: None,
2468                }]),
2469            ])),
2470        };
2471
2472        // Define inner function
2473        let inner_func = Ast::FunctionDefinition {
2474            name: "inner".to_string(),
2475            body: Box::new(Ast::Sequence(vec![
2476                Ast::Assignment {
2477                    var: "global_var".to_string(),
2478                    value: "inner_modified".to_string(),
2479                },
2480                Ast::Pipeline(vec![ShellCommand {
2481                    args: vec!["printf".to_string(), "inner_done".to_string()],
2482                    redirections: Vec::new(),
2483                    compound: None,
2484                }]),
2485            ])),
2486        };
2487
2488        // Define both functions
2489        execute(outer_func, &mut shell_state);
2490        execute(inner_func, &mut shell_state);
2491
2492        // Set initial global value
2493        shell_state.set_var("global_var", "initial".to_string());
2494
2495        // Call outer function (which calls inner function)
2496        let call_ast = Ast::FunctionCall {
2497            name: "outer".to_string(),
2498            args: vec![],
2499        };
2500        let exit_code = execute(call_ast, &mut shell_state);
2501        assert_eq!(exit_code, 0);
2502
2503        // After nested function calls, global variable should be modified by inner function
2504        // (bash behavior: function variable assignments affect global scope)
2505        assert_eq!(
2506            shell_state.get_var("global_var"),
2507            Some("inner_modified".to_string())
2508        );
2509    }
2510
2511    #[test]
2512    fn test_here_string_execution() {
2513        // Test here-string redirection with a simple command
2514        let cmd = ShellCommand {
2515            args: vec!["cat".to_string()],
2516            redirections: Vec::new(),
2517            compound: None,
2518            // TODO: Update test for new redirection system
2519        };
2520
2521        // Note: This test would require mocking stdin to provide the here-string content
2522        // For now, we'll just verify the command structure is parsed correctly
2523        assert_eq!(cmd.args, vec!["cat"]);
2524        // assert_eq!(cmd.here_string_content, Some("hello world".to_string()));
2525    }
2526
2527    #[test]
2528    fn test_here_document_execution() {
2529        // Test here-document redirection with a simple command
2530        let cmd = ShellCommand {
2531            args: vec!["cat".to_string()],
2532            redirections: Vec::new(),
2533            compound: None,
2534            // TODO: Update test for new redirection system
2535        };
2536
2537        // Note: This test would require mocking stdin to provide the here-document content
2538        // For now, we'll just verify the command structure is parsed correctly
2539        assert_eq!(cmd.args, vec!["cat"]);
2540        // assert_eq!(cmd.here_doc_delimiter, Some("EOF".to_string()));
2541    }
2542
2543    #[test]
2544    fn test_here_document_with_variable_expansion() {
2545        // Test that variables are expanded in here-document content
2546        let mut shell_state = ShellState::new();
2547        shell_state.set_var("PWD", "/test/path".to_string());
2548
2549        // Simulate here-doc content with variable
2550        let content = "Working dir: $PWD";
2551        let expanded = expand_variables_in_string(content, &mut shell_state);
2552
2553        assert_eq!(expanded, "Working dir: /test/path");
2554    }
2555
2556    #[test]
2557    fn test_here_document_with_command_substitution_builtin() {
2558        // Test that builtin command substitutions work in here-document content
2559        let mut shell_state = ShellState::new();
2560        shell_state.set_var("PWD", "/test/dir".to_string());
2561
2562        // Simulate here-doc content with pwd builtin command substitution
2563        let content = "Current directory: `pwd`";
2564        let expanded = expand_variables_in_string(content, &mut shell_state);
2565
2566        // The pwd builtin should be executed and expanded
2567        assert!(expanded.contains("Current directory: "));
2568    }
2569
2570    // ========================================================================
2571    // File Descriptor Integration Tests
2572    // ========================================================================
2573
2574    #[test]
2575    fn test_fd_output_redirection() {
2576        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2577
2578        // Create unique temp file
2579        use std::time::{SystemTime, UNIX_EPOCH};
2580        let timestamp = SystemTime::now()
2581            .duration_since(UNIX_EPOCH)
2582            .unwrap()
2583            .as_nanos();
2584        let temp_file = format!("/tmp/rush_test_fd_out_{}.txt", timestamp);
2585
2586        // Test: echo "error" 2>errors.txt
2587        let cmd = ShellCommand {
2588            args: vec![
2589                "sh".to_string(),
2590                "-c".to_string(),
2591                "echo error >&2".to_string(),
2592            ],
2593            redirections: vec![Redirection::FdOutput(2, temp_file.clone())],
2594            compound: None,
2595        };
2596
2597        let mut shell_state = ShellState::new();
2598        let exit_code = execute_single_command(&cmd, &mut shell_state);
2599        assert_eq!(exit_code, 0);
2600
2601        // Verify file was created and contains the error message
2602        let content = std::fs::read_to_string(&temp_file).unwrap();
2603        assert_eq!(content.trim(), "error");
2604
2605        // Cleanup
2606        let _ = std::fs::remove_file(&temp_file);
2607    }
2608
2609    #[test]
2610    fn test_fd_input_redirection() {
2611        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2612
2613        // Create unique temp file with content
2614        use std::time::{SystemTime, UNIX_EPOCH};
2615        let timestamp = SystemTime::now()
2616            .duration_since(UNIX_EPOCH)
2617            .unwrap()
2618            .as_nanos();
2619        let temp_file = format!("/tmp/rush_test_fd_in_{}.txt", timestamp);
2620
2621        std::fs::write(&temp_file, "test input\n").unwrap();
2622        std::thread::sleep(std::time::Duration::from_millis(10));
2623
2624        // Test: cat 3<input.txt (reading from fd 3)
2625        // Note: This tests that fd 3 is opened for reading
2626        let cmd = ShellCommand {
2627            args: vec!["cat".to_string()],
2628            compound: None,
2629            redirections: vec![
2630                Redirection::FdInput(3, temp_file.clone()),
2631                Redirection::Input(temp_file.clone()),
2632            ],
2633        };
2634
2635        let mut shell_state = ShellState::new();
2636        let exit_code = execute_single_command(&cmd, &mut shell_state);
2637        assert_eq!(exit_code, 0);
2638
2639        // Cleanup
2640        let _ = std::fs::remove_file(&temp_file);
2641    }
2642
2643    #[test]
2644    fn test_fd_append_redirection() {
2645        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2646
2647        // Create unique temp file with initial content
2648        use std::time::{SystemTime, UNIX_EPOCH};
2649        let timestamp = SystemTime::now()
2650            .duration_since(UNIX_EPOCH)
2651            .unwrap()
2652            .as_nanos();
2653        let temp_file = format!("/tmp/rush_test_fd_append_{}.txt", timestamp);
2654
2655        std::fs::write(&temp_file, "first line\n").unwrap();
2656        std::thread::sleep(std::time::Duration::from_millis(10));
2657
2658        // Test: echo "more" 2>>errors.txt
2659        let cmd = ShellCommand {
2660            args: vec![
2661                "sh".to_string(),
2662                "-c".to_string(),
2663                "echo second line >&2".to_string(),
2664            ],
2665            redirections: vec![Redirection::FdAppend(2, temp_file.clone())],
2666            compound: None,
2667        };
2668
2669        let mut shell_state = ShellState::new();
2670        let exit_code = execute_single_command(&cmd, &mut shell_state);
2671        assert_eq!(exit_code, 0);
2672
2673        // Verify file contains both lines
2674        let content = std::fs::read_to_string(&temp_file).unwrap();
2675        assert!(content.contains("first line"));
2676        assert!(content.contains("second line"));
2677
2678        // Cleanup
2679        let _ = std::fs::remove_file(&temp_file);
2680    }
2681
2682    #[test]
2683    fn test_fd_duplication_stderr_to_stdout() {
2684        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2685
2686        // Create unique temp file
2687        use std::time::{SystemTime, UNIX_EPOCH};
2688        let timestamp = SystemTime::now()
2689            .duration_since(UNIX_EPOCH)
2690            .unwrap()
2691            .as_nanos();
2692        let temp_file = format!("/tmp/rush_test_fd_dup_{}.txt", timestamp);
2693
2694        // Test: command 2>&1 >output.txt
2695        // Note: For external commands, fd duplication is handled by the shell
2696        // We test that the command executes successfully with the redirection
2697        let cmd = ShellCommand {
2698            args: vec![
2699                "sh".to_string(),
2700                "-c".to_string(),
2701                "echo test; echo error >&2".to_string(),
2702            ],
2703            compound: None,
2704            redirections: vec![Redirection::Output(temp_file.clone())],
2705        };
2706
2707        let mut shell_state = ShellState::new();
2708        let exit_code = execute_single_command(&cmd, &mut shell_state);
2709        assert_eq!(exit_code, 0);
2710
2711        // Verify file was created and contains output
2712        assert!(std::path::Path::new(&temp_file).exists());
2713        let content = std::fs::read_to_string(&temp_file).unwrap();
2714        assert!(content.contains("test"));
2715
2716        // Cleanup
2717        let _ = std::fs::remove_file(&temp_file);
2718    }
2719
2720    #[test]
2721    fn test_fd_close() {
2722        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2723
2724        // Test: command 2>&- (closes stderr)
2725        let cmd = ShellCommand {
2726            args: vec!["sh".to_string(), "-c".to_string(), "echo test".to_string()],
2727            redirections: vec![Redirection::FdClose(2)],
2728            compound: None,
2729        };
2730
2731        let mut shell_state = ShellState::new();
2732        let exit_code = execute_single_command(&cmd, &mut shell_state);
2733        assert_eq!(exit_code, 0);
2734
2735        // Verify fd 2 is closed in the fd table
2736        assert!(shell_state.fd_table.borrow().is_closed(2));
2737    }
2738
2739    #[test]
2740    fn test_fd_read_write() {
2741        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2742
2743        // Create unique temp file
2744        use std::time::{SystemTime, UNIX_EPOCH};
2745        let timestamp = SystemTime::now()
2746            .duration_since(UNIX_EPOCH)
2747            .unwrap()
2748            .as_nanos();
2749        let temp_file = format!("/tmp/rush_test_fd_rw_{}.txt", timestamp);
2750
2751        std::fs::write(&temp_file, "initial content\n").unwrap();
2752        std::thread::sleep(std::time::Duration::from_millis(10));
2753
2754        // Test: 3<>file.txt (opens fd 3 for read/write)
2755        let cmd = ShellCommand {
2756            args: vec!["cat".to_string()],
2757            compound: None,
2758            redirections: vec![
2759                Redirection::FdInputOutput(3, temp_file.clone()),
2760                Redirection::Input(temp_file.clone()),
2761            ],
2762        };
2763
2764        let mut shell_state = ShellState::new();
2765        let exit_code = execute_single_command(&cmd, &mut shell_state);
2766        assert_eq!(exit_code, 0);
2767
2768        // Cleanup
2769        let _ = std::fs::remove_file(&temp_file);
2770    }
2771
2772    #[test]
2773    fn test_multiple_fd_redirections() {
2774        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2775
2776        // Create unique temp files
2777        use std::time::{SystemTime, UNIX_EPOCH};
2778        let timestamp = SystemTime::now()
2779            .duration_since(UNIX_EPOCH)
2780            .unwrap()
2781            .as_nanos();
2782        let out_file = format!("/tmp/rush_test_fd_multi_out_{}.txt", timestamp);
2783        let err_file = format!("/tmp/rush_test_fd_multi_err_{}.txt", timestamp);
2784
2785        // Test: command 2>err.txt 1>out.txt
2786        let cmd = ShellCommand {
2787            args: vec![
2788                "sh".to_string(),
2789                "-c".to_string(),
2790                "echo stdout; echo stderr >&2".to_string(),
2791            ],
2792            redirections: vec![
2793                Redirection::FdOutput(2, err_file.clone()),
2794                Redirection::Output(out_file.clone()),
2795            ],
2796            compound: None,
2797        };
2798
2799        let mut shell_state = ShellState::new();
2800        let exit_code = execute_single_command(&cmd, &mut shell_state);
2801        assert_eq!(exit_code, 0);
2802
2803        // Verify both files were created
2804        assert!(std::path::Path::new(&out_file).exists());
2805        assert!(std::path::Path::new(&err_file).exists());
2806
2807        // Verify content
2808        let out_content = std::fs::read_to_string(&out_file).unwrap();
2809        let err_content = std::fs::read_to_string(&err_file).unwrap();
2810        assert!(out_content.contains("stdout"));
2811        assert!(err_content.contains("stderr"));
2812
2813        // Cleanup
2814        let _ = std::fs::remove_file(&out_file);
2815        let _ = std::fs::remove_file(&err_file);
2816    }
2817
2818    #[test]
2819    fn test_fd_swap_pattern() {
2820        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2821
2822        // Create unique temp files
2823        use std::time::{SystemTime, UNIX_EPOCH};
2824        let timestamp = SystemTime::now()
2825            .duration_since(UNIX_EPOCH)
2826            .unwrap()
2827            .as_nanos();
2828        let temp_file = format!("/tmp/rush_test_fd_swap_{}.txt", timestamp);
2829
2830        // Test fd operations: open fd 3, then close it
2831        // This tests the fd table operations
2832        let cmd = ShellCommand {
2833            args: vec!["sh".to_string(), "-c".to_string(), "echo test".to_string()],
2834            redirections: vec![
2835                Redirection::FdOutput(3, temp_file.clone()), // Open fd 3 for writing
2836                Redirection::FdClose(3),                     // Close fd 3
2837                Redirection::Output(temp_file.clone()),      // Write to stdout
2838            ],
2839            compound: None,
2840        };
2841
2842        let mut shell_state = ShellState::new();
2843        let exit_code = execute_single_command(&cmd, &mut shell_state);
2844        assert_eq!(exit_code, 0);
2845
2846        // Verify fd 3 is closed after the operations
2847        assert!(shell_state.fd_table.borrow().is_closed(3));
2848
2849        // Cleanup
2850        let _ = std::fs::remove_file(&temp_file);
2851    }
2852
2853    #[test]
2854    fn test_fd_redirection_with_pipes() {
2855        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2856
2857        // Create unique temp file
2858        use std::time::{SystemTime, UNIX_EPOCH};
2859        let timestamp = SystemTime::now()
2860            .duration_since(UNIX_EPOCH)
2861            .unwrap()
2862            .as_nanos();
2863        let temp_file = format!("/tmp/rush_test_fd_pipe_{}.txt", timestamp);
2864
2865        // Test: cmd1 | cmd2 >output.txt
2866        // This tests redirections in pipelines
2867        let commands = vec![
2868            ShellCommand {
2869                args: vec!["echo".to_string(), "piped output".to_string()],
2870                redirections: vec![],
2871                compound: None,
2872            },
2873            ShellCommand {
2874                args: vec!["cat".to_string()],
2875                compound: None,
2876                redirections: vec![Redirection::Output(temp_file.clone())],
2877            },
2878        ];
2879
2880        let mut shell_state = ShellState::new();
2881        let exit_code = execute_pipeline(&commands, &mut shell_state);
2882        assert_eq!(exit_code, 0);
2883
2884        // Verify output file contains the piped content
2885        let content = std::fs::read_to_string(&temp_file).unwrap();
2886        assert!(content.contains("piped output"));
2887
2888        // Cleanup
2889        let _ = std::fs::remove_file(&temp_file);
2890    }
2891
2892    #[test]
2893    fn test_fd_error_invalid_fd_number() {
2894        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2895
2896        // Create unique temp file
2897        use std::time::{SystemTime, UNIX_EPOCH};
2898        let timestamp = SystemTime::now()
2899            .duration_since(UNIX_EPOCH)
2900            .unwrap()
2901            .as_nanos();
2902        let temp_file = format!("/tmp/rush_test_fd_invalid_{}.txt", timestamp);
2903
2904        // Test: Invalid fd number (>1024)
2905        let cmd = ShellCommand {
2906            args: vec!["echo".to_string(), "test".to_string()],
2907            compound: None,
2908            redirections: vec![Redirection::FdOutput(1025, temp_file.clone())],
2909        };
2910
2911        let mut shell_state = ShellState::new();
2912        let exit_code = execute_single_command(&cmd, &mut shell_state);
2913
2914        // Should fail with error
2915        assert_eq!(exit_code, 1);
2916
2917        // Cleanup (file may not exist)
2918        let _ = std::fs::remove_file(&temp_file);
2919    }
2920
2921    #[test]
2922    fn test_fd_error_duplicate_closed_fd() {
2923        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2924
2925        // Test: Attempting to duplicate a closed fd
2926        let cmd = ShellCommand {
2927            args: vec!["echo".to_string(), "test".to_string()],
2928            compound: None,
2929            redirections: vec![
2930                Redirection::FdClose(3),
2931                Redirection::FdDuplicate(2, 3), // Try to duplicate closed fd 3
2932            ],
2933        };
2934
2935        let mut shell_state = ShellState::new();
2936        let exit_code = execute_single_command(&cmd, &mut shell_state);
2937
2938        // Should fail with error
2939        assert_eq!(exit_code, 1);
2940    }
2941
2942    #[test]
2943    fn test_fd_error_file_permission() {
2944        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2945
2946        // Test: Attempting to write to a read-only location
2947        let cmd = ShellCommand {
2948            args: vec!["echo".to_string(), "test".to_string()],
2949            redirections: vec![Redirection::FdOutput(2, "/proc/version".to_string())],
2950            compound: None,
2951        };
2952
2953        let mut shell_state = ShellState::new();
2954        let exit_code = execute_single_command(&cmd, &mut shell_state);
2955
2956        // Should fail with permission error
2957        assert_eq!(exit_code, 1);
2958    }
2959
2960    #[test]
2961    fn test_fd_redirection_order() {
2962        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2963
2964        // Create unique temp files
2965        use std::time::{SystemTime, UNIX_EPOCH};
2966        let timestamp = SystemTime::now()
2967            .duration_since(UNIX_EPOCH)
2968            .unwrap()
2969            .as_nanos();
2970        let file1 = format!("/tmp/rush_test_fd_order1_{}.txt", timestamp);
2971        let file2 = format!("/tmp/rush_test_fd_order2_{}.txt", timestamp);
2972
2973        // Test: Redirections are processed left-to-right
2974        // 1>file1 1>file2 should write to file2
2975        let cmd = ShellCommand {
2976            args: vec!["echo".to_string(), "test".to_string()],
2977            compound: None,
2978            redirections: vec![
2979                Redirection::Output(file1.clone()),
2980                Redirection::Output(file2.clone()),
2981            ],
2982        };
2983
2984        let mut shell_state = ShellState::new();
2985        let exit_code = execute_single_command(&cmd, &mut shell_state);
2986        assert_eq!(exit_code, 0);
2987
2988        // file2 should have the output (last redirection wins)
2989        let content2 = std::fs::read_to_string(&file2).unwrap();
2990        assert!(content2.contains("test"));
2991
2992        // Cleanup
2993        let _ = std::fs::remove_file(&file1);
2994        let _ = std::fs::remove_file(&file2);
2995    }
2996
2997    #[test]
2998    fn test_fd_builtin_with_redirection() {
2999        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3000
3001        // Create unique temp file
3002        use std::time::{SystemTime, UNIX_EPOCH};
3003        let timestamp = SystemTime::now()
3004            .duration_since(UNIX_EPOCH)
3005            .unwrap()
3006            .as_nanos();
3007        let temp_file = format!("/tmp/rush_test_fd_builtin_{}.txt", timestamp);
3008
3009        // Test: Built-in command with fd redirection
3010        let cmd = ShellCommand {
3011            args: vec!["echo".to_string(), "builtin test".to_string()],
3012            redirections: vec![Redirection::Output(temp_file.clone())],
3013            compound: None,
3014        };
3015
3016        let mut shell_state = ShellState::new();
3017        let exit_code = execute_single_command(&cmd, &mut shell_state);
3018        assert_eq!(exit_code, 0);
3019
3020        // Verify output
3021        let content = std::fs::read_to_string(&temp_file).unwrap();
3022        assert!(content.contains("builtin test"));
3023
3024        // Cleanup
3025        let _ = std::fs::remove_file(&temp_file);
3026    }
3027
3028    #[test]
3029    fn test_fd_variable_expansion_in_filename() {
3030        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3031
3032        // Create unique temp file
3033        use std::time::{SystemTime, UNIX_EPOCH};
3034        let timestamp = SystemTime::now()
3035            .duration_since(UNIX_EPOCH)
3036            .unwrap()
3037            .as_nanos();
3038        let temp_file = format!("/tmp/rush_test_fd_var_{}.txt", timestamp);
3039
3040        // Set variable for filename
3041        let mut shell_state = ShellState::new();
3042        shell_state.set_var("OUTFILE", temp_file.clone());
3043
3044        // Test: Variable expansion in redirection filename
3045        let cmd = ShellCommand {
3046            args: vec!["echo".to_string(), "variable test".to_string()],
3047            compound: None,
3048            redirections: vec![Redirection::Output("$OUTFILE".to_string())],
3049        };
3050
3051        let exit_code = execute_single_command(&cmd, &mut shell_state);
3052        assert_eq!(exit_code, 0);
3053
3054        // Verify output
3055        let content = std::fs::read_to_string(&temp_file).unwrap();
3056        assert!(content.contains("variable test"));
3057
3058        // Cleanup
3059        let _ = std::fs::remove_file(&temp_file);
3060    }
3061}