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, 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        }
696        // For builtins (command is None), the stdin is already handled by the shell's stdin
697        // The builtin will need to read from fd 0 which is already set up
698    } else {
699        // Custom fd - for external commands, we need to redirect the custom fd for reading
700        // Open the file (we need to keep the handle alive for the command)
701        let fd_file = File::open(&expanded_file)
702            .map_err(|e| format!("Cannot open {}: {}", expanded_file, e))?;
703
704        // For external commands, store both in fd table and prepare for stdin redirect
705        shell_state.fd_table.borrow_mut().open_fd(
706            fd,
707            &expanded_file,
708            true,  // read
709            false, // write
710            false, // append
711            false, // truncate
712        )?;
713
714        // If we have an external command, set up the file descriptor in the child process
715        if let Some(cmd) = command {
716            // Keep fd_file alive by moving it into the closure
717            // It will be dropped (and closed) when the closure is dropped in the parent
718            let target_fd = fd;
719            unsafe {
720                cmd.pre_exec(move || {
721                    let raw_fd = fd_file.as_raw_fd();
722
723                    // The inherited file descriptor might not be at the target fd number
724                    // Use dup2 to ensure it's at the correct fd number
725                    if raw_fd != target_fd {
726                        let result = libc::dup2(raw_fd, target_fd);
727                        if result < 0 {
728                            return Err(std::io::Error::last_os_error());
729                        }
730                        // We don't need to close raw_fd manually because fd_file
731                        // has CLOEXEC set by default and will be closed on exec
732                    }
733                    Ok(())
734                });
735            }
736        }
737    }
738
739    Ok(())
740}
741
742/// Apply output redirection for a specific file descriptor
743fn apply_output_redirection(
744    fd: i32,
745    file: &str,
746    append: bool,
747    shell_state: &mut ShellState,
748    command: Option<&mut Command>,
749) -> Result<(), String> {
750    let expanded_file = expand_variables_in_string(file, shell_state);
751
752    // Open file for writing or appending
753    let file_handle = if append {
754        OpenOptions::new()
755            .append(true)
756            .create(true)
757            .open(&expanded_file)
758            .map_err(|e| format!("Cannot open {}: {}", expanded_file, e))?
759    } else {
760        File::create(&expanded_file)
761            .map_err(|e| format!("Cannot create {}: {}", expanded_file, e))?
762    };
763
764    if fd == 1 {
765        // stdout redirection - apply to Command if present
766        if let Some(cmd) = command {
767            cmd.stdout(Stdio::from(file_handle));
768        }
769    } else if fd == 2 {
770        // stderr redirection - apply to Command if present
771        if let Some(cmd) = command {
772            cmd.stderr(Stdio::from(file_handle));
773        }
774    } else {
775        // Custom fd - store in fd table
776        shell_state.fd_table.borrow_mut().open_fd(
777            fd,
778            &expanded_file,
779            false, // read
780            true,  // write
781            append,
782            !append, // truncate if not appending
783        )?;
784    }
785
786    Ok(())
787}
788
789/// Apply file descriptor duplication
790fn apply_fd_duplication(
791    target_fd: i32,
792    source_fd: i32,
793    shell_state: &mut ShellState,
794    _command: Option<&mut Command>,
795) -> Result<(), String> {
796    // Check if source_fd is explicitly closed before attempting duplication
797    if shell_state.fd_table.borrow().is_closed(source_fd) {
798        let error_msg = format!("File descriptor {} is closed", source_fd);
799        if shell_state.colors_enabled {
800            eprintln!(
801                "{}Redirection error: {}\x1b[0m",
802                shell_state.color_scheme.error, error_msg
803            );
804        } else {
805            eprintln!("Redirection error: {}", error_msg);
806        }
807        return Err(error_msg);
808    }
809
810    // Duplicate source_fd to target_fd
811    shell_state
812        .fd_table
813        .borrow_mut()
814        .duplicate_fd(source_fd, target_fd)?;
815    Ok(())
816}
817
818/// Apply file descriptor closing
819fn apply_fd_close(
820    fd: i32,
821    shell_state: &mut ShellState,
822    command: Option<&mut Command>,
823) -> Result<(), String> {
824    // Close the specified fd in the fd table
825    shell_state.fd_table.borrow_mut().close_fd(fd)?;
826
827    // For external commands, we need to redirect the fd to /dev/null
828    // This ensures that writes to the closed fd don't produce errors
829    if let Some(cmd) = command {
830        match fd {
831            0 => {
832                // Close stdin - redirect to /dev/null for reading
833                cmd.stdin(Stdio::null());
834            }
835            1 => {
836                // Close stdout - redirect to /dev/null for writing
837                cmd.stdout(Stdio::null());
838            }
839            2 => {
840                // Close stderr - redirect to /dev/null for writing
841                cmd.stderr(Stdio::null());
842            }
843            _ => {
844                // For custom fds (3+), we use pre_exec to close them
845                // This is handled via the fd_table and dup2 operations
846            }
847        }
848    }
849
850    Ok(())
851}
852
853/// Apply read/write file descriptor opening
854fn apply_fd_input_output(
855    fd: i32,
856    file: &str,
857    shell_state: &mut ShellState,
858    _command: Option<&mut Command>,
859) -> Result<(), String> {
860    let expanded_file = expand_variables_in_string(file, shell_state);
861
862    // Open file for both reading and writing
863    shell_state.fd_table.borrow_mut().open_fd(
864        fd,
865        &expanded_file,
866        true,  // read
867        true,  // write
868        false, // append
869        false, // truncate
870    )?;
871
872    Ok(())
873}
874
875/// Apply here-document redirection
876fn apply_heredoc_redirection(
877    fd: i32,
878    delimiter: &str,
879    quoted: bool,
880    shell_state: &mut ShellState,
881    command: Option<&mut Command>,
882) -> Result<(), String> {
883    let here_doc_content = collect_here_document_content(delimiter, shell_state);
884
885    // Expand variables and command substitutions ONLY if delimiter was not quoted
886    let expanded_content = if quoted {
887        here_doc_content
888    } else {
889        expand_variables_in_string(&here_doc_content, shell_state)
890    };
891
892    // Create a pipe and write the content
893    let (reader, mut writer) =
894        pipe().map_err(|e| format!("Failed to create pipe for here-document: {}", e))?;
895
896    writeln!(writer, "{}", expanded_content)
897        .map_err(|e| format!("Failed to write here-document content: {}", e))?;
898
899    // Apply to stdin if fd is 0
900    if fd == 0 {
901        if let Some(cmd) = command {
902            cmd.stdin(Stdio::from(reader));
903        }
904    }
905
906    Ok(())
907}
908
909/// Apply here-string redirection
910fn apply_herestring_redirection(
911    fd: i32,
912    content: &str,
913    shell_state: &mut ShellState,
914    command: Option<&mut Command>,
915) -> Result<(), String> {
916    let expanded_content = expand_variables_in_string(content, shell_state);
917
918    // Create a pipe and write the content
919    let (reader, mut writer) =
920        pipe().map_err(|e| format!("Failed to create pipe for here-string: {}", e))?;
921
922    write!(writer, "{}", expanded_content)
923        .map_err(|e| format!("Failed to write here-string content: {}", e))?;
924
925    // Apply to stdin if fd is 0
926    if fd == 0 {
927        if let Some(cmd) = command {
928            cmd.stdin(Stdio::from(reader));
929        }
930    }
931
932    Ok(())
933}
934
935/// Execute a trap handler command
936/// Note: Signal masking during trap execution will be added in a future update
937pub fn execute_trap_handler(trap_cmd: &str, shell_state: &mut ShellState) -> i32 {
938    // Save current exit code to preserve it across trap execution
939    let saved_exit_code = shell_state.last_exit_code;
940
941    // TODO: Add signal masking to prevent recursive trap calls
942    // This requires careful handling of the nix sigprocmask API
943    // For now, traps execute without signal masking
944
945    // Parse and execute the trap command
946    let result = match crate::lexer::lex(trap_cmd, shell_state) {
947        Ok(tokens) => {
948            match crate::lexer::expand_aliases(
949                tokens,
950                shell_state,
951                &mut std::collections::HashSet::new(),
952            ) {
953                Ok(expanded_tokens) => {
954                    match crate::parser::parse(expanded_tokens) {
955                        Ok(ast) => execute(ast, shell_state),
956                        Err(_) => {
957                            // Parse error in trap handler - silently continue
958                            saved_exit_code
959                        }
960                    }
961                }
962                Err(_) => {
963                    // Alias expansion error - silently continue
964                    saved_exit_code
965                }
966            }
967        }
968        Err(_) => {
969            // Lex error in trap handler - silently continue
970            saved_exit_code
971        }
972    };
973
974    // Restore the original exit code (trap handlers don't affect $?)
975    shell_state.last_exit_code = saved_exit_code;
976
977    result
978}
979
980pub fn execute(ast: Ast, shell_state: &mut ShellState) -> i32 {
981    match ast {
982        Ast::Assignment { var, value } => {
983            // Expand variables and command substitutions in the value
984            let expanded_value = expand_variables_in_string(&value, shell_state);
985            shell_state.set_var(&var, expanded_value);
986            0
987        }
988        Ast::LocalAssignment { var, value } => {
989            // Expand variables and command substitutions in the value
990            let expanded_value = expand_variables_in_string(&value, shell_state);
991            shell_state.set_local_var(&var, expanded_value);
992            0
993        }
994        Ast::Pipeline(commands) => {
995            if commands.is_empty() {
996                return 0;
997            }
998
999            if commands.len() == 1 {
1000                // Single command, handle redirections
1001                execute_single_command(&commands[0], shell_state)
1002            } else {
1003                // Pipeline
1004                execute_pipeline(&commands, shell_state)
1005            }
1006        }
1007        Ast::Sequence(asts) => {
1008            let mut exit_code = 0;
1009            for ast in asts {
1010                exit_code = execute(ast, shell_state);
1011
1012                // Check if we got an early return from a function
1013                if shell_state.is_returning() {
1014                    return exit_code;
1015                }
1016
1017                // Check if exit was requested (e.g., from trap handler)
1018                if shell_state.exit_requested {
1019                    return shell_state.exit_code;
1020                }
1021            }
1022            exit_code
1023        }
1024        Ast::If {
1025            branches,
1026            else_branch,
1027        } => {
1028            for (condition, then_branch) in branches {
1029                let cond_exit = execute(*condition, shell_state);
1030                if cond_exit == 0 {
1031                    let exit_code = execute(*then_branch, shell_state);
1032
1033                    // Check if we got an early return from a function
1034                    if shell_state.is_returning() {
1035                        return exit_code;
1036                    }
1037
1038                    return exit_code;
1039                }
1040            }
1041            if let Some(else_b) = else_branch {
1042                let exit_code = execute(*else_b, shell_state);
1043
1044                // Check if we got an early return from a function
1045                if shell_state.is_returning() {
1046                    return exit_code;
1047                }
1048
1049                exit_code
1050            } else {
1051                0
1052            }
1053        }
1054        Ast::Case {
1055            word,
1056            cases,
1057            default,
1058        } => {
1059            for (patterns, branch) in cases {
1060                for pattern in &patterns {
1061                    if let Ok(glob_pattern) = glob::Pattern::new(pattern) {
1062                        if glob_pattern.matches(&word) {
1063                            let exit_code = execute(branch, shell_state);
1064
1065                            // Check if we got an early return from a function
1066                            if shell_state.is_returning() {
1067                                return exit_code;
1068                            }
1069
1070                            return exit_code;
1071                        }
1072                    } else {
1073                        // If pattern is invalid, fall back to exact match
1074                        if &word == pattern {
1075                            let exit_code = execute(branch, shell_state);
1076
1077                            // Check if we got an early return from a function
1078                            if shell_state.is_returning() {
1079                                return exit_code;
1080                            }
1081
1082                            return exit_code;
1083                        }
1084                    }
1085                }
1086            }
1087            if let Some(def) = default {
1088                let exit_code = execute(*def, shell_state);
1089
1090                // 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::For {
1101            variable,
1102            items,
1103            body,
1104        } => {
1105            let mut exit_code = 0;
1106
1107            // Execute the loop body for each item
1108            for item in items {
1109                // Process any pending signals before executing the body
1110                crate::state::process_pending_signals(shell_state);
1111
1112                // Check if exit was requested (e.g., from trap handler)
1113                if shell_state.exit_requested {
1114                    return shell_state.exit_code;
1115                }
1116
1117                // Set the loop variable
1118                shell_state.set_var(&variable, item.clone());
1119
1120                // Execute the body
1121                exit_code = execute(*body.clone(), 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                // Check if exit was requested after executing the body
1129                if shell_state.exit_requested {
1130                    return shell_state.exit_code;
1131                }
1132            }
1133
1134            exit_code
1135        }
1136        Ast::While { condition, body } => {
1137            let mut exit_code = 0;
1138
1139            // Execute the loop while condition is true (exit code 0)
1140            loop {
1141                // Evaluate the condition
1142                let cond_exit = execute(*condition.clone(), shell_state);
1143
1144                // Check if we got an early return from a function
1145                if shell_state.is_returning() {
1146                    return cond_exit;
1147                }
1148
1149                // Check if exit was requested (e.g., from trap handler)
1150                if shell_state.exit_requested {
1151                    return shell_state.exit_code;
1152                }
1153
1154                // If condition is false (non-zero exit code), break
1155                if cond_exit != 0 {
1156                    break;
1157                }
1158
1159                // Execute the body
1160                exit_code = execute(*body.clone(), shell_state);
1161
1162                // Check if we got an early return from a function
1163                if shell_state.is_returning() {
1164                    return exit_code;
1165                }
1166
1167                // Check if exit was requested (e.g., from trap handler)
1168                if shell_state.exit_requested {
1169                    return shell_state.exit_code;
1170                }
1171            }
1172
1173            exit_code
1174        }
1175        Ast::FunctionDefinition { name, body } => {
1176            // Store function definition in shell state
1177            shell_state.define_function(name.clone(), *body);
1178            0
1179        }
1180        Ast::FunctionCall { name, args } => {
1181            if let Some(function_body) = shell_state.get_function(&name).cloned() {
1182                // Check recursion limit before entering function
1183                if shell_state.function_depth >= shell_state.max_recursion_depth {
1184                    eprintln!(
1185                        "Function recursion limit ({}) exceeded",
1186                        shell_state.max_recursion_depth
1187                    );
1188                    return 1;
1189                }
1190
1191                // Enter function context for local variable scoping
1192                shell_state.enter_function();
1193
1194                // Set up arguments as regular variables (will be enhanced in Phase 2)
1195                let old_positional = shell_state.positional_params.clone();
1196
1197                // Set positional parameters for function arguments
1198                shell_state.set_positional_params(args.clone());
1199
1200                // Execute function body
1201                let exit_code = execute(function_body, shell_state);
1202
1203                // Check if we got an early return from the function
1204                if shell_state.is_returning() {
1205                    let return_value = shell_state.get_return_value().unwrap_or(0);
1206
1207                    // Restore old positional parameters
1208                    shell_state.set_positional_params(old_positional);
1209
1210                    // Exit function context
1211                    shell_state.exit_function();
1212
1213                    // Clear return state
1214                    shell_state.clear_return();
1215
1216                    // Return the early return value
1217                    return return_value;
1218                }
1219
1220                // Restore old positional parameters
1221                shell_state.set_positional_params(old_positional);
1222
1223                // Exit function context
1224                shell_state.exit_function();
1225
1226                exit_code
1227            } else {
1228                eprintln!("Function '{}' not found", name);
1229                1
1230            }
1231        }
1232        Ast::Return { value } => {
1233            // Return statements can only be used inside functions
1234            if shell_state.function_depth == 0 {
1235                eprintln!("Return statement outside of function");
1236                return 1;
1237            }
1238
1239            // Parse return value if provided
1240            let exit_code = if let Some(ref val) = value {
1241                val.parse::<i32>().unwrap_or(0)
1242            } else {
1243                0
1244            };
1245
1246            // Set return state to indicate early return from function
1247            shell_state.set_return(exit_code);
1248
1249            // Return the exit code - the function call handler will check for this
1250            exit_code
1251        }
1252        Ast::And { left, right } => {
1253            // Execute left side first
1254            let left_exit = execute(*left, shell_state);
1255
1256            // Check if we got an early return from a function
1257            if shell_state.is_returning() {
1258                return left_exit;
1259            }
1260
1261            // Only execute right side if left succeeded (exit code 0)
1262            if left_exit == 0 {
1263                execute(*right, shell_state)
1264            } else {
1265                left_exit
1266            }
1267        }
1268        Ast::Or { left, right } => {
1269            // Execute left side first
1270            let left_exit = execute(*left, shell_state);
1271
1272            // Check if we got an early return from a function
1273            if shell_state.is_returning() {
1274                return left_exit;
1275            }
1276
1277            // Only execute right side if left failed (exit code != 0)
1278            if left_exit != 0 {
1279                execute(*right, shell_state)
1280            } else {
1281                left_exit
1282            }
1283        }
1284        Ast::Subshell { body } => execute_subshell(*body, shell_state),
1285    }
1286}
1287
1288fn execute_single_command(cmd: &ShellCommand, shell_state: &mut ShellState) -> i32 {
1289    // Check if this is a compound command (subshell)
1290    if let Some(ref compound_ast) = cmd.compound {
1291        // Execute compound command with redirections
1292        return execute_compound_with_redirections(compound_ast, shell_state, &cmd.redirections);
1293    }
1294
1295    if cmd.args.is_empty() {
1296        // No command, but may have redirections - process them for side effects
1297        if !cmd.redirections.is_empty() {
1298            if let Err(e) = apply_redirections(&cmd.redirections, shell_state, None) {
1299                if shell_state.colors_enabled {
1300                    eprintln!(
1301                        "{}Redirection error: {}\x1b[0m",
1302                        shell_state.color_scheme.error, e
1303                    );
1304                } else {
1305                    eprintln!("Redirection error: {}", e);
1306                }
1307                return 1;
1308            }
1309        }
1310        return 0;
1311    }
1312
1313    // First expand variables, then wildcards
1314    let var_expanded_args = expand_variables_in_args(&cmd.args, shell_state);
1315    let expanded_args = match expand_wildcards(&var_expanded_args) {
1316        Ok(args) => args,
1317        Err(_) => return 1,
1318    };
1319
1320    if expanded_args.is_empty() {
1321        return 0;
1322    }
1323
1324    // Check if this is a function call
1325    if shell_state.get_function(&expanded_args[0]).is_some() {
1326        // This is a function call - create a FunctionCall AST node and execute it
1327        let function_call = Ast::FunctionCall {
1328            name: expanded_args[0].clone(),
1329            args: expanded_args[1..].to_vec(),
1330        };
1331        return execute(function_call, shell_state);
1332    }
1333
1334    if crate::builtins::is_builtin(&expanded_args[0]) {
1335        // Create a temporary ShellCommand with expanded args
1336        let temp_cmd = ShellCommand {
1337            args: expanded_args,
1338            redirections: cmd.redirections.clone(),
1339            compound: None,
1340        };
1341
1342        // If we're capturing output, create a writer for it
1343        if let Some(ref capture_buffer) = shell_state.capture_output.clone() {
1344            // Create a writer that writes to our capture buffer
1345            struct CaptureWriter {
1346                buffer: Rc<RefCell<Vec<u8>>>,
1347            }
1348            impl std::io::Write for CaptureWriter {
1349                fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1350                    self.buffer.borrow_mut().extend_from_slice(buf);
1351                    Ok(buf.len())
1352                }
1353                fn flush(&mut self) -> std::io::Result<()> {
1354                    Ok(())
1355                }
1356            }
1357            let writer = CaptureWriter {
1358                buffer: capture_buffer.clone(),
1359            };
1360            crate::builtins::execute_builtin(&temp_cmd, shell_state, Some(Box::new(writer)))
1361        } else {
1362            crate::builtins::execute_builtin(&temp_cmd, shell_state, None)
1363        }
1364    } else {
1365        // Separate environment variable assignments from the actual command
1366        // Environment vars must come before the command and have the form VAR=value
1367        let mut env_assignments = Vec::new();
1368        let mut command_start_idx = 0;
1369
1370        for (idx, arg) in expanded_args.iter().enumerate() {
1371            // Check if this looks like an environment variable assignment
1372            if let Some(eq_pos) = arg.find('=')
1373                && eq_pos > 0
1374            {
1375                let var_part = &arg[..eq_pos];
1376                // Check if var_part is a valid variable name
1377                if var_part
1378                    .chars()
1379                    .next()
1380                    .map(|c| c.is_alphabetic() || c == '_')
1381                    .unwrap_or(false)
1382                    && var_part.chars().all(|c| c.is_alphanumeric() || c == '_')
1383                {
1384                    env_assignments.push(arg.clone());
1385                    command_start_idx = idx + 1;
1386                    continue;
1387                }
1388            }
1389            // If we reach here, this is not an env assignment, so we've found the command
1390            break;
1391        }
1392
1393        // Check if we have a command to execute (vs just env assignments)
1394        let has_command = command_start_idx < expanded_args.len();
1395
1396        // If all args were env assignments, set them in the shell
1397        // but continue to process redirections per POSIX
1398        if !has_command {
1399            for assignment in &env_assignments {
1400                if let Some(eq_pos) = assignment.find('=') {
1401                    let var_name = &assignment[..eq_pos];
1402                    let var_value = &assignment[eq_pos + 1..];
1403                    shell_state.set_var(var_name, var_value.to_string());
1404                }
1405            }
1406
1407            // Process redirections even without a command
1408            if !cmd.redirections.is_empty() {
1409                if let Err(e) = apply_redirections(&cmd.redirections, shell_state, None) {
1410                    if shell_state.colors_enabled {
1411                        eprintln!(
1412                            "{}Redirection error: {}\x1b[0m",
1413                            shell_state.color_scheme.error, e
1414                        );
1415                    } else {
1416                        eprintln!("Redirection error: {}", e);
1417                    }
1418                    return 1;
1419                }
1420            }
1421            return 0;
1422        }
1423
1424        // Prepare command
1425        let mut command = Command::new(&expanded_args[command_start_idx]);
1426        command.args(&expanded_args[command_start_idx + 1..]);
1427
1428        // Check for stdin override (for pipeline subshells)
1429        if let Some(fd) = shell_state.stdin_override {
1430            unsafe {
1431                let dup_fd = libc::dup(fd);
1432                if dup_fd >= 0 {
1433                    command.stdin(Stdio::from_raw_fd(dup_fd));
1434                }
1435            }
1436        }
1437
1438        // Set environment for child process
1439        let mut child_env = shell_state.get_env_for_child();
1440
1441        // Add the per-command environment variable assignments
1442        for assignment in env_assignments {
1443            if let Some(eq_pos) = assignment.find('=') {
1444                let var_name = assignment[..eq_pos].to_string();
1445                let var_value = assignment[eq_pos + 1..].to_string();
1446                child_env.insert(var_name, var_value);
1447            }
1448        }
1449
1450        command.env_clear();
1451        for (key, value) in child_env {
1452            command.env(key, value);
1453        }
1454
1455        // If we're capturing output, redirect stdout to capture buffer
1456        let capturing = shell_state.capture_output.is_some();
1457        if capturing {
1458            command.stdout(Stdio::piped());
1459        }
1460
1461        // Apply all redirections
1462        if let Err(e) = apply_redirections(&cmd.redirections, shell_state, Some(&mut command)) {
1463            if shell_state.colors_enabled {
1464                eprintln!(
1465                    "{}Redirection error: {}\x1b[0m",
1466                    shell_state.color_scheme.error, e
1467                );
1468            } else {
1469                eprintln!("Redirection error: {}", e);
1470            }
1471            return 1;
1472        }
1473
1474        // Apply custom file descriptors (3-9) from fd table to external command
1475        // We need to keep the FD table borrowed until after the child is spawned
1476        // to prevent File handles from being dropped and FDs from being closed
1477        let custom_fds: Vec<(i32, RawFd)> = {
1478            let fd_table = shell_state.fd_table.borrow();
1479            let mut fds = Vec::new();
1480
1481            for fd_num in 3..=9 {
1482                if fd_table.is_open(fd_num) {
1483                    if let Some(raw_fd) = fd_table.get_raw_fd(fd_num) {
1484                        fds.push((fd_num, raw_fd));
1485                    }
1486                }
1487            }
1488
1489            fds
1490        };
1491
1492        // If we have custom fds to apply, use pre_exec to set them in the child
1493        if !custom_fds.is_empty() {
1494            unsafe {
1495                command.pre_exec(move || {
1496                    for (target_fd, source_fd) in &custom_fds {
1497                        let result = libc::dup2(*source_fd, *target_fd);
1498                        if result < 0 {
1499                            return Err(std::io::Error::last_os_error());
1500                        }
1501                    }
1502                    Ok(())
1503                });
1504            }
1505        }
1506
1507        // Spawn and execute the command
1508        // Note: The FD table borrow above has been released, but the custom_fds
1509        // closure capture keeps the file handles alive
1510        match command.spawn() {
1511            Ok(mut child) => {
1512                // If capturing, read stdout
1513                if capturing {
1514                    if let Some(mut stdout) = child.stdout.take() {
1515                        use std::io::Read;
1516                        let mut output = Vec::new();
1517                        if stdout.read_to_end(&mut output).is_ok() {
1518                            if let Some(ref capture_buffer) = shell_state.capture_output {
1519                                capture_buffer.borrow_mut().extend_from_slice(&output);
1520                            }
1521                        }
1522                    }
1523                }
1524
1525                match child.wait() {
1526                    Ok(status) => status.code().unwrap_or(0),
1527                    Err(e) => {
1528                        if shell_state.colors_enabled {
1529                            eprintln!(
1530                                "{}Error waiting for command: {}\x1b[0m",
1531                                shell_state.color_scheme.error, e
1532                            );
1533                        } else {
1534                            eprintln!("Error waiting for command: {}", e);
1535                        }
1536                        1
1537                    }
1538                }
1539            }
1540            Err(e) => {
1541                if shell_state.colors_enabled {
1542                    eprintln!(
1543                        "{}Command spawn error: {}\x1b[0m",
1544                        shell_state.color_scheme.error, e
1545                    );
1546                } else {
1547                    eprintln!("Command spawn error: {}", e);
1548                }
1549                1
1550            }
1551        }
1552    }
1553}
1554
1555fn execute_pipeline(commands: &[ShellCommand], shell_state: &mut ShellState) -> i32 {
1556    let mut exit_code = 0;
1557    let mut previous_stdout = None;
1558
1559    for (i, cmd) in commands.iter().enumerate() {
1560        let is_last = i == commands.len() - 1;
1561
1562        // Check if this is a compound command (subshell)
1563        if let Some(ref compound_ast) = cmd.compound {
1564            // Execute compound command (subshell) in pipeline
1565            exit_code = execute_compound_in_pipeline(
1566                compound_ast,
1567                shell_state,
1568                i == 0,
1569                is_last,
1570                &cmd.redirections,
1571            );
1572
1573            // For Phase 2, compound commands in pipelines don't produce stdout for next stage
1574            // This will be enhanced in Phase 3 with proper pipe handling
1575            previous_stdout = None;
1576            continue;
1577        }
1578
1579        if cmd.args.is_empty() {
1580            continue;
1581        }
1582
1583        // First expand variables, then wildcards
1584        let var_expanded_args = expand_variables_in_args(&cmd.args, shell_state);
1585        let expanded_args = match expand_wildcards(&var_expanded_args) {
1586            Ok(args) => args,
1587            Err(_) => return 1,
1588        };
1589
1590        if expanded_args.is_empty() {
1591            continue;
1592        }
1593
1594        if crate::builtins::is_builtin(&expanded_args[0]) {
1595            // Built-ins in pipelines are tricky - for now, execute them separately
1596            // This is not perfect but better than nothing
1597            let temp_cmd = ShellCommand {
1598                args: expanded_args,
1599                redirections: cmd.redirections.clone(),
1600                compound: None,
1601            };
1602            if !is_last {
1603                // Create a safe pipe
1604                let (reader, writer) = match pipe() {
1605                    Ok(p) => p,
1606                    Err(e) => {
1607                        if shell_state.colors_enabled {
1608                            eprintln!(
1609                                "{}Error creating pipe for builtin: {}\x1b[0m",
1610                                shell_state.color_scheme.error, e
1611                            );
1612                        } else {
1613                            eprintln!("Error creating pipe for builtin: {}", e);
1614                        }
1615                        return 1;
1616                    }
1617                };
1618                // Execute builtin with writer for output capture
1619                exit_code = crate::builtins::execute_builtin(
1620                    &temp_cmd,
1621                    shell_state,
1622                    Some(Box::new(writer)),
1623                );
1624                // Use reader for next command's stdin
1625                previous_stdout = Some(Stdio::from(reader));
1626            } else {
1627                // Last command: check if we're capturing output
1628                if let Some(ref capture_buffer) = shell_state.capture_output.clone() {
1629                    // Create a writer that writes to our capture buffer
1630                    struct CaptureWriter {
1631                        buffer: Rc<RefCell<Vec<u8>>>,
1632                    }
1633                    impl std::io::Write for CaptureWriter {
1634                        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1635                            self.buffer.borrow_mut().extend_from_slice(buf);
1636                            Ok(buf.len())
1637                        }
1638                        fn flush(&mut self) -> std::io::Result<()> {
1639                            Ok(())
1640                        }
1641                    }
1642                    let writer = CaptureWriter {
1643                        buffer: capture_buffer.clone(),
1644                    };
1645                    exit_code = crate::builtins::execute_builtin(
1646                        &temp_cmd,
1647                        shell_state,
1648                        Some(Box::new(writer)),
1649                    );
1650                } else {
1651                    // Not capturing, execute normally
1652                    exit_code = crate::builtins::execute_builtin(&temp_cmd, shell_state, None);
1653                }
1654                previous_stdout = None;
1655            }
1656        } else {
1657            let mut command = Command::new(&expanded_args[0]);
1658            command.args(&expanded_args[1..]);
1659
1660            // Set environment for child process
1661            let child_env = shell_state.get_env_for_child();
1662            command.env_clear();
1663            for (key, value) in child_env {
1664                command.env(key, value);
1665            }
1666
1667            // Set stdin from previous command's stdout
1668            if let Some(prev) = previous_stdout.take() {
1669                command.stdin(prev);
1670            } else if i > 0 {
1671                // We are in a pipeline (not first command) but have no input pipe.
1672                // This means the previous command didn't produce a pipe.
1673                // We should treat this as empty input (EOF), not inherit stdin!
1674                command.stdin(Stdio::null());
1675            } else if let Some(fd) = shell_state.stdin_override {
1676                // We have a stdin override (e.g. from parent subshell)
1677                // We must duplicate it because Stdio takes ownership
1678                unsafe {
1679                    let dup_fd = libc::dup(fd);
1680                    if dup_fd >= 0 {
1681                        command.stdin(Stdio::from_raw_fd(dup_fd));
1682                    }
1683                }
1684            }
1685
1686            // Set stdout for next command, or for capturing if this is the last
1687            if !is_last {
1688                command.stdout(Stdio::piped());
1689            } else if shell_state.capture_output.is_some() {
1690                // Last command in pipeline but we're capturing output
1691                command.stdout(Stdio::piped());
1692            }
1693
1694            // Apply redirections for this command
1695            if let Err(e) = apply_redirections(&cmd.redirections, shell_state, Some(&mut command)) {
1696                if shell_state.colors_enabled {
1697                    eprintln!(
1698                        "{}Redirection error: {}\x1b[0m",
1699                        shell_state.color_scheme.error, e
1700                    );
1701                } else {
1702                    eprintln!("Redirection error: {}", e);
1703                }
1704                return 1;
1705            }
1706
1707            match command.spawn() {
1708                Ok(mut child) => {
1709                    if !is_last {
1710                        previous_stdout = child.stdout.take().map(Stdio::from);
1711                    } else if shell_state.capture_output.is_some() {
1712                        // Last command and we're capturing - read its output
1713                        if let Some(mut stdout) = child.stdout.take() {
1714                            use std::io::Read;
1715                            let mut output = Vec::new();
1716                            if stdout.read_to_end(&mut output).is_ok()
1717                                && let Some(ref capture_buffer) = shell_state.capture_output
1718                            {
1719                                capture_buffer.borrow_mut().extend_from_slice(&output);
1720                            }
1721                        }
1722                    }
1723                    match child.wait() {
1724                        Ok(status) => {
1725                            exit_code = status.code().unwrap_or(0);
1726                        }
1727                        Err(e) => {
1728                            if shell_state.colors_enabled {
1729                                eprintln!(
1730                                    "{}Error waiting for command: {}\x1b[0m",
1731                                    shell_state.color_scheme.error, e
1732                                );
1733                            } else {
1734                                eprintln!("Error waiting for command: {}", e);
1735                            }
1736                            exit_code = 1;
1737                        }
1738                    }
1739                }
1740                Err(e) => {
1741                    if shell_state.colors_enabled {
1742                        eprintln!(
1743                            "{}Error spawning command '{}{}",
1744                            shell_state.color_scheme.error,
1745                            expanded_args[0],
1746                            &format!("': {}\x1b[0m", e)
1747                        );
1748                    } else {
1749                        eprintln!("Error spawning command '{}': {}", expanded_args[0], e);
1750                    }
1751                    exit_code = 1;
1752                }
1753            }
1754        }
1755    }
1756
1757    exit_code
1758}
1759
1760/// Execute a subshell with isolated state
1761///
1762/// # Arguments
1763/// * `body` - The AST to execute in the subshell
1764/// * `shell_state` - The parent shell state (will be cloned)
1765///
1766/// # Returns
1767/// * Exit code from the subshell execution
1768///
1769/// # Behavior
1770/// - Clones the shell state for isolation
1771/// - Executes the body in the cloned state
1772/// - Returns the exit code without modifying parent state
1773/// - Preserves parent state completely (variables, functions, etc.)
1774/// - Tracks subshell depth to prevent stack overflow
1775/// - Handles exit and return commands properly (isolated from parent)
1776/// - Cleans up file descriptors to prevent resource leaks
1777fn execute_subshell(body: Ast, shell_state: &mut ShellState) -> i32 {
1778    // Check depth limit to prevent stack overflow
1779    if shell_state.subshell_depth >= MAX_SUBSHELL_DEPTH {
1780        if shell_state.colors_enabled {
1781            eprintln!(
1782                "{}Subshell nesting limit ({}) exceeded\x1b[0m",
1783                shell_state.color_scheme.error, MAX_SUBSHELL_DEPTH
1784            );
1785        } else {
1786            eprintln!("Subshell nesting limit ({}) exceeded", MAX_SUBSHELL_DEPTH);
1787        }
1788        shell_state.last_exit_code = 1;
1789        return 1;
1790    }
1791
1792    // Save current directory for restoration
1793    let original_dir = std::env::current_dir().ok();
1794
1795    // Clone the shell state for isolation
1796    let mut subshell_state = shell_state.clone();
1797
1798    // Deep clone the file descriptor table for isolation
1799    // shell_state.clone() only clones the Rc, so we need to manually deep clone the table
1800    // and put it in a new Rc<RefCell<_>>
1801    match shell_state.fd_table.borrow().deep_clone() {
1802        Ok(new_fd_table) => {
1803            subshell_state.fd_table = Rc::new(RefCell::new(new_fd_table));
1804        }
1805        Err(e) => {
1806            if shell_state.colors_enabled {
1807                eprintln!(
1808                    "{}Failed to clone file descriptor table: {}\x1b[0m",
1809                    shell_state.color_scheme.error, e
1810                );
1811            } else {
1812                eprintln!("Failed to clone file descriptor table: {}", e);
1813            }
1814            return 1;
1815        }
1816    }
1817
1818    // Increment subshell depth in the cloned state
1819    subshell_state.subshell_depth = shell_state.subshell_depth + 1;
1820
1821    // Clone trap handlers for isolation (subshells inherit but don't affect parent)
1822    let parent_traps = shell_state.trap_handlers.lock().unwrap().clone();
1823    subshell_state.trap_handlers = std::sync::Arc::new(std::sync::Mutex::new(parent_traps));
1824
1825    // Execute the body in the isolated state
1826    let exit_code = execute(body, &mut subshell_state);
1827
1828    // Handle exit in subshell: exit should only exit the subshell, not the parent
1829    // The exit_requested flag is isolated to the subshell_state, so it won't affect parent
1830    let final_exit_code = if subshell_state.exit_requested {
1831        // Subshell called exit - use its exit code
1832        subshell_state.exit_code
1833    } else if subshell_state.is_returning() {
1834        // Subshell called return - treat as exit from subshell
1835        // Return in subshell should not propagate to parent function
1836        subshell_state.get_return_value().unwrap_or(exit_code)
1837    } else {
1838        exit_code
1839    };
1840
1841    // Clean up the subshell's file descriptor table to prevent resource leaks
1842    // This ensures any file descriptors opened in the subshell are properly released
1843    subshell_state.fd_table.borrow_mut().clear();
1844
1845    // Restore original directory (in case subshell changed it)
1846    if let Some(dir) = original_dir {
1847        let _ = std::env::set_current_dir(dir);
1848    }
1849
1850    // Update parent's last_exit_code to reflect subshell result
1851    shell_state.last_exit_code = final_exit_code;
1852
1853    // Return the exit code
1854    final_exit_code
1855}
1856
1857/// Execute a compound command with redirections
1858///
1859/// # Arguments
1860/// * `compound_ast` - The compound command AST
1861/// * `shell_state` - The shell state
1862/// * `redirections` - Redirections to apply
1863///
1864/// # Returns
1865/// * Exit code from the compound command
1866fn execute_compound_with_redirections(
1867    compound_ast: &Ast,
1868    shell_state: &mut ShellState,
1869    redirections: &[Redirection],
1870) -> i32 {
1871    match compound_ast {
1872        Ast::Subshell { body } => {
1873            // For subshells with redirections, we need to:
1874            // 1. Set up output capture if there are output redirections
1875            // 2. Execute the subshell
1876            // 3. Apply the redirections to the captured output
1877
1878            // Check if we have output redirections
1879            let has_output_redir = redirections.iter().any(|r| {
1880                matches!(
1881                    r,
1882                    Redirection::Output(_)
1883                        | Redirection::Append(_)
1884                        | Redirection::FdOutput(_, _)
1885                        | Redirection::FdAppend(_, _)
1886                )
1887            });
1888
1889            if has_output_redir {
1890                // Clone state for subshell
1891                let mut subshell_state = shell_state.clone();
1892
1893                // Set up output capture
1894                let capture_buffer = Rc::new(RefCell::new(Vec::new()));
1895                subshell_state.capture_output = Some(capture_buffer.clone());
1896
1897                // Execute subshell
1898                let exit_code = execute(*body.clone(), &mut subshell_state);
1899
1900                // Get captured output
1901                let output = capture_buffer.borrow().clone();
1902
1903                // Apply redirections to output
1904                for redir in redirections {
1905                    match redir {
1906                        Redirection::Output(file) => {
1907                            let expanded_file = expand_variables_in_string(file, shell_state);
1908                            if let Err(e) = std::fs::write(&expanded_file, &output) {
1909                                if shell_state.colors_enabled {
1910                                    eprintln!(
1911                                        "{}Redirection error: {}\x1b[0m",
1912                                        shell_state.color_scheme.error, e
1913                                    );
1914                                } else {
1915                                    eprintln!("Redirection error: {}", e);
1916                                }
1917                                return 1;
1918                            }
1919                        }
1920                        Redirection::Append(file) => {
1921                            let expanded_file = expand_variables_in_string(file, shell_state);
1922                            use std::fs::OpenOptions;
1923                            let mut file_handle = match OpenOptions::new()
1924                                .append(true)
1925                                .create(true)
1926                                .open(&expanded_file)
1927                            {
1928                                Ok(f) => f,
1929                                Err(e) => {
1930                                    if shell_state.colors_enabled {
1931                                        eprintln!(
1932                                            "{}Redirection error: {}\x1b[0m",
1933                                            shell_state.color_scheme.error, e
1934                                        );
1935                                    } else {
1936                                        eprintln!("Redirection error: {}", e);
1937                                    }
1938                                    return 1;
1939                                }
1940                            };
1941                            if let Err(e) = file_handle.write_all(&output) {
1942                                if shell_state.colors_enabled {
1943                                    eprintln!(
1944                                        "{}Redirection error: {}\x1b[0m",
1945                                        shell_state.color_scheme.error, e
1946                                    );
1947                                } else {
1948                                    eprintln!("Redirection error: {}", e);
1949                                }
1950                                return 1;
1951                            }
1952                        }
1953                        _ => {
1954                            // For Phase 2, only support basic output redirections
1955                            // Other redirections are silently ignored for subshells
1956                        }
1957                    }
1958                }
1959
1960                shell_state.last_exit_code = exit_code;
1961                exit_code
1962            } else {
1963                // No output redirections, execute normally
1964                execute_subshell(*body.clone(), shell_state)
1965            }
1966        }
1967        _ => {
1968            eprintln!("Unsupported compound command type");
1969            1
1970        }
1971    }
1972}
1973
1974/// Execute a compound command (subshell) as part of a pipeline
1975///
1976/// # Arguments
1977/// * `compound_ast` - The compound command AST (typically Subshell)
1978/// * `shell_state` - The parent shell state
1979/// * `is_last` - Whether this is the last command in the pipeline
1980/// * `redirections` - Redirections to apply to the compound command
1981///
1982/// # Returns
1983/// * Exit code from the compound command
1984fn execute_compound_in_pipeline(
1985    compound_ast: &Ast,
1986    shell_state: &mut ShellState,
1987    is_first: bool,
1988    is_last: bool,
1989    _redirections: &[Redirection],
1990) -> i32 {
1991    match compound_ast {
1992        Ast::Subshell { body } => {
1993            // Clone state for subshell
1994            let mut subshell_state = shell_state.clone();
1995
1996            // If we are not the first command in pipeline, and since we don't receive input (broken pipe),
1997            // we use stdin_override to force reading from /dev/null, without modifying process-global 0.
1998            // We keep the file handle alive for the duration of the subshell execution.
1999            let _null_file = if !is_first {
2000                if let Ok(f) = File::open("/dev/null") {
2001                    subshell_state.stdin_override = Some(f.as_raw_fd());
2002                    Some(f)
2003                } else {
2004                    None
2005                }
2006            } else {
2007                None
2008            };
2009
2010            // Execute subshell with appropriate stdout capture
2011            let exit_code = if !is_last || shell_state.capture_output.is_some() {
2012                // Need to capture subshell output
2013                let capture_buffer = Rc::new(RefCell::new(Vec::new()));
2014                subshell_state.capture_output = Some(capture_buffer.clone());
2015
2016                // Execute subshell
2017                let code = execute(*body.clone(), &mut subshell_state);
2018
2019                // Transfer captured output to parent's capture buffer
2020                if let Some(ref parent_capture) = shell_state.capture_output {
2021                    let captured = capture_buffer.borrow().clone();
2022                    parent_capture.borrow_mut().extend_from_slice(&captured);
2023                }
2024
2025                // Update parent's last_exit_code
2026                shell_state.last_exit_code = code;
2027
2028                code
2029            } else {
2030                // Last command, no capture needed
2031                let code = execute(*body.clone(), &mut subshell_state);
2032                shell_state.last_exit_code = code;
2033                code
2034            };
2035
2036            exit_code
2037        }
2038        _ => {
2039            // Other compound commands not yet supported
2040            eprintln!("Unsupported compound command in pipeline");
2041            1
2042        }
2043    }
2044}
2045
2046#[cfg(test)]
2047mod tests {
2048    use super::*;
2049    use std::sync::Mutex;
2050
2051    // Mutex to serialize tests that modify environment variables or create files
2052    static ENV_LOCK: Mutex<()> = Mutex::new(());
2053
2054    #[test]
2055    fn test_execute_single_command_builtin() {
2056        let cmd = ShellCommand {
2057            args: vec!["true".to_string()],
2058            redirections: Vec::new(),
2059            compound: None,
2060        };
2061        let mut shell_state = ShellState::new();
2062        let exit_code = execute_single_command(&cmd, &mut shell_state);
2063        assert_eq!(exit_code, 0);
2064    }
2065
2066    // For external commands, test with a command that exists
2067    #[test]
2068    fn test_execute_single_command_external() {
2069        let cmd = ShellCommand {
2070            args: vec!["true".to_string()], // Assume true exists
2071            redirections: Vec::new(),
2072            compound: None,
2073        };
2074        let mut shell_state = ShellState::new();
2075        let exit_code = execute_single_command(&cmd, &mut shell_state);
2076        assert_eq!(exit_code, 0);
2077    }
2078
2079    #[test]
2080    fn test_execute_single_command_external_nonexistent() {
2081        let cmd = ShellCommand {
2082            args: vec!["nonexistent_command".to_string()],
2083            redirections: Vec::new(),
2084            compound: None,
2085        };
2086        let mut shell_state = ShellState::new();
2087        let exit_code = execute_single_command(&cmd, &mut shell_state);
2088        assert_eq!(exit_code, 1); // Command not found
2089    }
2090
2091    #[test]
2092    fn test_execute_pipeline() {
2093        let commands = vec![
2094            ShellCommand {
2095                args: vec!["printf".to_string(), "hello".to_string()],
2096                redirections: Vec::new(),
2097                compound: None,
2098            },
2099            ShellCommand {
2100                args: vec!["cat".to_string()], // cat reads from stdin
2101                redirections: Vec::new(),
2102                compound: None,
2103            },
2104        ];
2105        let mut shell_state = ShellState::new();
2106        let exit_code = execute_pipeline(&commands, &mut shell_state);
2107        assert_eq!(exit_code, 0);
2108    }
2109
2110    #[test]
2111    fn test_execute_empty_pipeline() {
2112        let commands = vec![];
2113        let mut shell_state = ShellState::new();
2114        let exit_code = execute(Ast::Pipeline(commands), &mut shell_state);
2115        assert_eq!(exit_code, 0);
2116    }
2117
2118    #[test]
2119    fn test_execute_single_command() {
2120        let ast = Ast::Pipeline(vec![ShellCommand {
2121            args: vec!["true".to_string()],
2122            redirections: Vec::new(),
2123            compound: None,
2124        }]);
2125        let mut shell_state = ShellState::new();
2126        let exit_code = execute(ast, &mut shell_state);
2127        assert_eq!(exit_code, 0);
2128    }
2129
2130    #[test]
2131    fn test_execute_function_definition() {
2132        let ast = Ast::FunctionDefinition {
2133            name: "test_func".to_string(),
2134            body: Box::new(Ast::Pipeline(vec![ShellCommand {
2135                args: vec!["echo".to_string(), "hello".to_string()],
2136                redirections: Vec::new(),
2137                compound: None,
2138            }])),
2139        };
2140        let mut shell_state = ShellState::new();
2141        let exit_code = execute(ast, &mut shell_state);
2142        assert_eq!(exit_code, 0);
2143
2144        // Check that function was stored
2145        assert!(shell_state.get_function("test_func").is_some());
2146    }
2147
2148    #[test]
2149    fn test_execute_function_call() {
2150        // First define a function
2151        let mut shell_state = ShellState::new();
2152        shell_state.define_function(
2153            "test_func".to_string(),
2154            Ast::Pipeline(vec![ShellCommand {
2155                args: vec!["echo".to_string(), "hello".to_string()],
2156                redirections: Vec::new(),
2157                compound: None,
2158            }]),
2159        );
2160
2161        // Now call the function
2162        let ast = Ast::FunctionCall {
2163            name: "test_func".to_string(),
2164            args: vec![],
2165        };
2166        let exit_code = execute(ast, &mut shell_state);
2167        assert_eq!(exit_code, 0);
2168    }
2169
2170    #[test]
2171    fn test_execute_function_call_with_args() {
2172        // First define a function that uses arguments
2173        let mut shell_state = ShellState::new();
2174        shell_state.define_function(
2175            "test_func".to_string(),
2176            Ast::Pipeline(vec![ShellCommand {
2177                args: vec!["echo".to_string(), "arg1".to_string()],
2178                redirections: Vec::new(),
2179                compound: None,
2180            }]),
2181        );
2182
2183        // Now call the function with arguments
2184        let ast = Ast::FunctionCall {
2185            name: "test_func".to_string(),
2186            args: vec!["hello".to_string()],
2187        };
2188        let exit_code = execute(ast, &mut shell_state);
2189        assert_eq!(exit_code, 0);
2190    }
2191
2192    #[test]
2193    fn test_execute_nonexistent_function() {
2194        let mut shell_state = ShellState::new();
2195        let ast = Ast::FunctionCall {
2196            name: "nonexistent".to_string(),
2197            args: vec![],
2198        };
2199        let exit_code = execute(ast, &mut shell_state);
2200        assert_eq!(exit_code, 1); // Should return error code
2201    }
2202
2203    #[test]
2204    fn test_execute_function_integration() {
2205        // Test full integration: define function, then call it
2206        let mut shell_state = ShellState::new();
2207
2208        // First define a function
2209        let define_ast = Ast::FunctionDefinition {
2210            name: "hello".to_string(),
2211            body: Box::new(Ast::Pipeline(vec![ShellCommand {
2212                args: vec!["printf".to_string(), "Hello from function".to_string()],
2213                redirections: Vec::new(),
2214                compound: None,
2215            }])),
2216        };
2217        let exit_code = execute(define_ast, &mut shell_state);
2218        assert_eq!(exit_code, 0);
2219
2220        // Now call the function
2221        let call_ast = Ast::FunctionCall {
2222            name: "hello".to_string(),
2223            args: vec![],
2224        };
2225        let exit_code = execute(call_ast, &mut shell_state);
2226        assert_eq!(exit_code, 0);
2227    }
2228
2229    #[test]
2230    fn test_execute_function_with_local_variables() {
2231        let mut shell_state = ShellState::new();
2232
2233        // Set a global variable
2234        shell_state.set_var("global_var", "global_value".to_string());
2235
2236        // Define a function that uses local variables
2237        let define_ast = Ast::FunctionDefinition {
2238            name: "test_func".to_string(),
2239            body: Box::new(Ast::Sequence(vec![
2240                Ast::LocalAssignment {
2241                    var: "local_var".to_string(),
2242                    value: "local_value".to_string(),
2243                },
2244                Ast::Assignment {
2245                    var: "global_var".to_string(),
2246                    value: "modified_in_function".to_string(),
2247                },
2248                Ast::Pipeline(vec![ShellCommand {
2249                    args: vec!["printf".to_string(), "success".to_string()],
2250                    redirections: Vec::new(),
2251                    compound: None,
2252                }]),
2253            ])),
2254        };
2255        let exit_code = execute(define_ast, &mut shell_state);
2256        assert_eq!(exit_code, 0);
2257
2258        // Global variable should not be modified during function definition
2259        assert_eq!(
2260            shell_state.get_var("global_var"),
2261            Some("global_value".to_string())
2262        );
2263
2264        // Call the function
2265        let call_ast = Ast::FunctionCall {
2266            name: "test_func".to_string(),
2267            args: vec![],
2268        };
2269        let exit_code = execute(call_ast, &mut shell_state);
2270        assert_eq!(exit_code, 0);
2271
2272        // After function call, global variable should be modified since function assignments affect global scope
2273        assert_eq!(
2274            shell_state.get_var("global_var"),
2275            Some("modified_in_function".to_string())
2276        );
2277    }
2278
2279    #[test]
2280    fn test_execute_nested_function_calls() {
2281        let mut shell_state = ShellState::new();
2282
2283        // Set global variable
2284        shell_state.set_var("global_var", "global".to_string());
2285
2286        // Define outer function
2287        let outer_func = Ast::FunctionDefinition {
2288            name: "outer".to_string(),
2289            body: Box::new(Ast::Sequence(vec![
2290                Ast::Assignment {
2291                    var: "global_var".to_string(),
2292                    value: "outer_modified".to_string(),
2293                },
2294                Ast::FunctionCall {
2295                    name: "inner".to_string(),
2296                    args: vec![],
2297                },
2298                Ast::Pipeline(vec![ShellCommand {
2299                    args: vec!["printf".to_string(), "outer_done".to_string()],
2300                    redirections: Vec::new(),
2301                    compound: None,
2302                }]),
2303            ])),
2304        };
2305
2306        // Define inner function
2307        let inner_func = Ast::FunctionDefinition {
2308            name: "inner".to_string(),
2309            body: Box::new(Ast::Sequence(vec![
2310                Ast::Assignment {
2311                    var: "global_var".to_string(),
2312                    value: "inner_modified".to_string(),
2313                },
2314                Ast::Pipeline(vec![ShellCommand {
2315                    args: vec!["printf".to_string(), "inner_done".to_string()],
2316                    redirections: Vec::new(),
2317                    compound: None,
2318                }]),
2319            ])),
2320        };
2321
2322        // Define both functions
2323        execute(outer_func, &mut shell_state);
2324        execute(inner_func, &mut shell_state);
2325
2326        // Set initial global value
2327        shell_state.set_var("global_var", "initial".to_string());
2328
2329        // Call outer function (which calls inner function)
2330        let call_ast = Ast::FunctionCall {
2331            name: "outer".to_string(),
2332            args: vec![],
2333        };
2334        let exit_code = execute(call_ast, &mut shell_state);
2335        assert_eq!(exit_code, 0);
2336
2337        // After nested function calls, global variable should be modified by inner function
2338        // (bash behavior: function variable assignments affect global scope)
2339        assert_eq!(
2340            shell_state.get_var("global_var"),
2341            Some("inner_modified".to_string())
2342        );
2343    }
2344
2345    #[test]
2346    fn test_here_string_execution() {
2347        // Test here-string redirection with a simple command
2348        let cmd = ShellCommand {
2349            args: vec!["cat".to_string()],
2350            redirections: Vec::new(),
2351            compound: None,
2352            // TODO: Update test for new redirection system
2353        };
2354
2355        // Note: This test would require mocking stdin to provide the here-string content
2356        // For now, we'll just verify the command structure is parsed correctly
2357        assert_eq!(cmd.args, vec!["cat"]);
2358        // assert_eq!(cmd.here_string_content, Some("hello world".to_string()));
2359    }
2360
2361    #[test]
2362    fn test_here_document_execution() {
2363        // Test here-document redirection with a simple command
2364        let cmd = ShellCommand {
2365            args: vec!["cat".to_string()],
2366            redirections: Vec::new(),
2367            compound: None,
2368            // TODO: Update test for new redirection system
2369        };
2370
2371        // Note: This test would require mocking stdin to provide the here-document content
2372        // For now, we'll just verify the command structure is parsed correctly
2373        assert_eq!(cmd.args, vec!["cat"]);
2374        // assert_eq!(cmd.here_doc_delimiter, Some("EOF".to_string()));
2375    }
2376
2377    #[test]
2378    fn test_here_document_with_variable_expansion() {
2379        // Test that variables are expanded in here-document content
2380        let mut shell_state = ShellState::new();
2381        shell_state.set_var("PWD", "/test/path".to_string());
2382
2383        // Simulate here-doc content with variable
2384        let content = "Working dir: $PWD";
2385        let expanded = expand_variables_in_string(content, &mut shell_state);
2386
2387        assert_eq!(expanded, "Working dir: /test/path");
2388    }
2389
2390    #[test]
2391    fn test_here_document_with_command_substitution_builtin() {
2392        // Test that builtin command substitutions work in here-document content
2393        let mut shell_state = ShellState::new();
2394        shell_state.set_var("PWD", "/test/dir".to_string());
2395
2396        // Simulate here-doc content with pwd builtin command substitution
2397        let content = "Current directory: `pwd`";
2398        let expanded = expand_variables_in_string(content, &mut shell_state);
2399
2400        // The pwd builtin should be executed and expanded
2401        assert!(expanded.contains("Current directory: "));
2402    }
2403
2404    // ========================================================================
2405    // File Descriptor Integration Tests
2406    // ========================================================================
2407
2408    #[test]
2409    fn test_fd_output_redirection() {
2410        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2411
2412        // Create unique temp file
2413        use std::time::{SystemTime, UNIX_EPOCH};
2414        let timestamp = SystemTime::now()
2415            .duration_since(UNIX_EPOCH)
2416            .unwrap()
2417            .as_nanos();
2418        let temp_file = format!("/tmp/rush_test_fd_out_{}.txt", timestamp);
2419
2420        // Test: echo "error" 2>errors.txt
2421        let cmd = ShellCommand {
2422            args: vec![
2423                "sh".to_string(),
2424                "-c".to_string(),
2425                "echo error >&2".to_string(),
2426            ],
2427            redirections: vec![Redirection::FdOutput(2, temp_file.clone())],
2428            compound: None,
2429        };
2430
2431        let mut shell_state = ShellState::new();
2432        let exit_code = execute_single_command(&cmd, &mut shell_state);
2433        assert_eq!(exit_code, 0);
2434
2435        // Verify file was created and contains the error message
2436        let content = std::fs::read_to_string(&temp_file).unwrap();
2437        assert_eq!(content.trim(), "error");
2438
2439        // Cleanup
2440        let _ = std::fs::remove_file(&temp_file);
2441    }
2442
2443    #[test]
2444    fn test_fd_input_redirection() {
2445        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2446
2447        // Create unique temp file with content
2448        use std::time::{SystemTime, UNIX_EPOCH};
2449        let timestamp = SystemTime::now()
2450            .duration_since(UNIX_EPOCH)
2451            .unwrap()
2452            .as_nanos();
2453        let temp_file = format!("/tmp/rush_test_fd_in_{}.txt", timestamp);
2454
2455        std::fs::write(&temp_file, "test input\n").unwrap();
2456        std::thread::sleep(std::time::Duration::from_millis(10));
2457
2458        // Test: cat 3<input.txt (reading from fd 3)
2459        // Note: This tests that fd 3 is opened for reading
2460        let cmd = ShellCommand {
2461            args: vec!["cat".to_string()],
2462            compound: None,
2463            redirections: vec![
2464                Redirection::FdInput(3, temp_file.clone()),
2465                Redirection::Input(temp_file.clone()),
2466            ],
2467        };
2468
2469        let mut shell_state = ShellState::new();
2470        let exit_code = execute_single_command(&cmd, &mut shell_state);
2471        assert_eq!(exit_code, 0);
2472
2473        // Cleanup
2474        let _ = std::fs::remove_file(&temp_file);
2475    }
2476
2477    #[test]
2478    fn test_fd_append_redirection() {
2479        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2480
2481        // Create unique temp file with initial content
2482        use std::time::{SystemTime, UNIX_EPOCH};
2483        let timestamp = SystemTime::now()
2484            .duration_since(UNIX_EPOCH)
2485            .unwrap()
2486            .as_nanos();
2487        let temp_file = format!("/tmp/rush_test_fd_append_{}.txt", timestamp);
2488
2489        std::fs::write(&temp_file, "first line\n").unwrap();
2490        std::thread::sleep(std::time::Duration::from_millis(10));
2491
2492        // Test: echo "more" 2>>errors.txt
2493        let cmd = ShellCommand {
2494            args: vec![
2495                "sh".to_string(),
2496                "-c".to_string(),
2497                "echo second line >&2".to_string(),
2498            ],
2499            redirections: vec![Redirection::FdAppend(2, temp_file.clone())],
2500            compound: None,
2501        };
2502
2503        let mut shell_state = ShellState::new();
2504        let exit_code = execute_single_command(&cmd, &mut shell_state);
2505        assert_eq!(exit_code, 0);
2506
2507        // Verify file contains both lines
2508        let content = std::fs::read_to_string(&temp_file).unwrap();
2509        assert!(content.contains("first line"));
2510        assert!(content.contains("second line"));
2511
2512        // Cleanup
2513        let _ = std::fs::remove_file(&temp_file);
2514    }
2515
2516    #[test]
2517    fn test_fd_duplication_stderr_to_stdout() {
2518        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2519
2520        // Create unique temp file
2521        use std::time::{SystemTime, UNIX_EPOCH};
2522        let timestamp = SystemTime::now()
2523            .duration_since(UNIX_EPOCH)
2524            .unwrap()
2525            .as_nanos();
2526        let temp_file = format!("/tmp/rush_test_fd_dup_{}.txt", timestamp);
2527
2528        // Test: command 2>&1 >output.txt
2529        // Note: For external commands, fd duplication is handled by the shell
2530        // We test that the command executes successfully with the redirection
2531        let cmd = ShellCommand {
2532            args: vec![
2533                "sh".to_string(),
2534                "-c".to_string(),
2535                "echo test; echo error >&2".to_string(),
2536            ],
2537            compound: None,
2538            redirections: vec![Redirection::Output(temp_file.clone())],
2539        };
2540
2541        let mut shell_state = ShellState::new();
2542        let exit_code = execute_single_command(&cmd, &mut shell_state);
2543        assert_eq!(exit_code, 0);
2544
2545        // Verify file was created and contains output
2546        assert!(std::path::Path::new(&temp_file).exists());
2547        let content = std::fs::read_to_string(&temp_file).unwrap();
2548        assert!(content.contains("test"));
2549
2550        // Cleanup
2551        let _ = std::fs::remove_file(&temp_file);
2552    }
2553
2554    #[test]
2555    fn test_fd_close() {
2556        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2557
2558        // Test: command 2>&- (closes stderr)
2559        let cmd = ShellCommand {
2560            args: vec!["sh".to_string(), "-c".to_string(), "echo test".to_string()],
2561            redirections: vec![Redirection::FdClose(2)],
2562            compound: None,
2563        };
2564
2565        let mut shell_state = ShellState::new();
2566        let exit_code = execute_single_command(&cmd, &mut shell_state);
2567        assert_eq!(exit_code, 0);
2568
2569        // Verify fd 2 is closed in the fd table
2570        assert!(shell_state.fd_table.borrow().is_closed(2));
2571    }
2572
2573    #[test]
2574    fn test_fd_read_write() {
2575        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2576
2577        // Create unique temp file
2578        use std::time::{SystemTime, UNIX_EPOCH};
2579        let timestamp = SystemTime::now()
2580            .duration_since(UNIX_EPOCH)
2581            .unwrap()
2582            .as_nanos();
2583        let temp_file = format!("/tmp/rush_test_fd_rw_{}.txt", timestamp);
2584
2585        std::fs::write(&temp_file, "initial content\n").unwrap();
2586        std::thread::sleep(std::time::Duration::from_millis(10));
2587
2588        // Test: 3<>file.txt (opens fd 3 for read/write)
2589        let cmd = ShellCommand {
2590            args: vec!["cat".to_string()],
2591            compound: None,
2592            redirections: vec![
2593                Redirection::FdInputOutput(3, temp_file.clone()),
2594                Redirection::Input(temp_file.clone()),
2595            ],
2596        };
2597
2598        let mut shell_state = ShellState::new();
2599        let exit_code = execute_single_command(&cmd, &mut shell_state);
2600        assert_eq!(exit_code, 0);
2601
2602        // Cleanup
2603        let _ = std::fs::remove_file(&temp_file);
2604    }
2605
2606    #[test]
2607    fn test_multiple_fd_redirections() {
2608        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2609
2610        // Create unique temp files
2611        use std::time::{SystemTime, UNIX_EPOCH};
2612        let timestamp = SystemTime::now()
2613            .duration_since(UNIX_EPOCH)
2614            .unwrap()
2615            .as_nanos();
2616        let out_file = format!("/tmp/rush_test_fd_multi_out_{}.txt", timestamp);
2617        let err_file = format!("/tmp/rush_test_fd_multi_err_{}.txt", timestamp);
2618
2619        // Test: command 2>err.txt 1>out.txt
2620        let cmd = ShellCommand {
2621            args: vec![
2622                "sh".to_string(),
2623                "-c".to_string(),
2624                "echo stdout; echo stderr >&2".to_string(),
2625            ],
2626            redirections: vec![
2627                Redirection::FdOutput(2, err_file.clone()),
2628                Redirection::Output(out_file.clone()),
2629            ],
2630            compound: None,
2631        };
2632
2633        let mut shell_state = ShellState::new();
2634        let exit_code = execute_single_command(&cmd, &mut shell_state);
2635        assert_eq!(exit_code, 0);
2636
2637        // Verify both files were created
2638        assert!(std::path::Path::new(&out_file).exists());
2639        assert!(std::path::Path::new(&err_file).exists());
2640
2641        // Verify content
2642        let out_content = std::fs::read_to_string(&out_file).unwrap();
2643        let err_content = std::fs::read_to_string(&err_file).unwrap();
2644        assert!(out_content.contains("stdout"));
2645        assert!(err_content.contains("stderr"));
2646
2647        // Cleanup
2648        let _ = std::fs::remove_file(&out_file);
2649        let _ = std::fs::remove_file(&err_file);
2650    }
2651
2652    #[test]
2653    fn test_fd_swap_pattern() {
2654        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2655
2656        // Create unique temp files
2657        use std::time::{SystemTime, UNIX_EPOCH};
2658        let timestamp = SystemTime::now()
2659            .duration_since(UNIX_EPOCH)
2660            .unwrap()
2661            .as_nanos();
2662        let temp_file = format!("/tmp/rush_test_fd_swap_{}.txt", timestamp);
2663
2664        // Test fd operations: open fd 3, then close it
2665        // This tests the fd table operations
2666        let cmd = ShellCommand {
2667            args: vec!["sh".to_string(), "-c".to_string(), "echo test".to_string()],
2668            redirections: vec![
2669                Redirection::FdOutput(3, temp_file.clone()), // Open fd 3 for writing
2670                Redirection::FdClose(3),                     // Close fd 3
2671                Redirection::Output(temp_file.clone()),      // Write to stdout
2672            ],
2673            compound: None,
2674        };
2675
2676        let mut shell_state = ShellState::new();
2677        let exit_code = execute_single_command(&cmd, &mut shell_state);
2678        assert_eq!(exit_code, 0);
2679
2680        // Verify fd 3 is closed after the operations
2681        assert!(shell_state.fd_table.borrow().is_closed(3));
2682
2683        // Cleanup
2684        let _ = std::fs::remove_file(&temp_file);
2685    }
2686
2687    #[test]
2688    fn test_fd_redirection_with_pipes() {
2689        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2690
2691        // Create unique temp file
2692        use std::time::{SystemTime, UNIX_EPOCH};
2693        let timestamp = SystemTime::now()
2694            .duration_since(UNIX_EPOCH)
2695            .unwrap()
2696            .as_nanos();
2697        let temp_file = format!("/tmp/rush_test_fd_pipe_{}.txt", timestamp);
2698
2699        // Test: cmd1 | cmd2 >output.txt
2700        // This tests redirections in pipelines
2701        let commands = vec![
2702            ShellCommand {
2703                args: vec!["echo".to_string(), "piped output".to_string()],
2704                redirections: vec![],
2705                compound: None,
2706            },
2707            ShellCommand {
2708                args: vec!["cat".to_string()],
2709                compound: None,
2710                redirections: vec![Redirection::Output(temp_file.clone())],
2711            },
2712        ];
2713
2714        let mut shell_state = ShellState::new();
2715        let exit_code = execute_pipeline(&commands, &mut shell_state);
2716        assert_eq!(exit_code, 0);
2717
2718        // Verify output file contains the piped content
2719        let content = std::fs::read_to_string(&temp_file).unwrap();
2720        assert!(content.contains("piped output"));
2721
2722        // Cleanup
2723        let _ = std::fs::remove_file(&temp_file);
2724    }
2725
2726    #[test]
2727    fn test_fd_error_invalid_fd_number() {
2728        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2729
2730        // Create unique temp file
2731        use std::time::{SystemTime, UNIX_EPOCH};
2732        let timestamp = SystemTime::now()
2733            .duration_since(UNIX_EPOCH)
2734            .unwrap()
2735            .as_nanos();
2736        let temp_file = format!("/tmp/rush_test_fd_invalid_{}.txt", timestamp);
2737
2738        // Test: Invalid fd number (>1024)
2739        let cmd = ShellCommand {
2740            args: vec!["echo".to_string(), "test".to_string()],
2741            compound: None,
2742            redirections: vec![Redirection::FdOutput(1025, temp_file.clone())],
2743        };
2744
2745        let mut shell_state = ShellState::new();
2746        let exit_code = execute_single_command(&cmd, &mut shell_state);
2747
2748        // Should fail with error
2749        assert_eq!(exit_code, 1);
2750
2751        // Cleanup (file may not exist)
2752        let _ = std::fs::remove_file(&temp_file);
2753    }
2754
2755    #[test]
2756    fn test_fd_error_duplicate_closed_fd() {
2757        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2758
2759        // Test: Attempting to duplicate a closed fd
2760        let cmd = ShellCommand {
2761            args: vec!["echo".to_string(), "test".to_string()],
2762            compound: None,
2763            redirections: vec![
2764                Redirection::FdClose(3),
2765                Redirection::FdDuplicate(2, 3), // Try to duplicate closed fd 3
2766            ],
2767        };
2768
2769        let mut shell_state = ShellState::new();
2770        let exit_code = execute_single_command(&cmd, &mut shell_state);
2771
2772        // Should fail with error
2773        assert_eq!(exit_code, 1);
2774    }
2775
2776    #[test]
2777    fn test_fd_error_file_permission() {
2778        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2779
2780        // Test: Attempting to write to a read-only location
2781        let cmd = ShellCommand {
2782            args: vec!["echo".to_string(), "test".to_string()],
2783            redirections: vec![Redirection::FdOutput(2, "/proc/version".to_string())],
2784            compound: None,
2785        };
2786
2787        let mut shell_state = ShellState::new();
2788        let exit_code = execute_single_command(&cmd, &mut shell_state);
2789
2790        // Should fail with permission error
2791        assert_eq!(exit_code, 1);
2792    }
2793
2794    #[test]
2795    fn test_fd_redirection_order() {
2796        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2797
2798        // Create unique temp files
2799        use std::time::{SystemTime, UNIX_EPOCH};
2800        let timestamp = SystemTime::now()
2801            .duration_since(UNIX_EPOCH)
2802            .unwrap()
2803            .as_nanos();
2804        let file1 = format!("/tmp/rush_test_fd_order1_{}.txt", timestamp);
2805        let file2 = format!("/tmp/rush_test_fd_order2_{}.txt", timestamp);
2806
2807        // Test: Redirections are processed left-to-right
2808        // 1>file1 1>file2 should write to file2
2809        let cmd = ShellCommand {
2810            args: vec!["echo".to_string(), "test".to_string()],
2811            compound: None,
2812            redirections: vec![
2813                Redirection::Output(file1.clone()),
2814                Redirection::Output(file2.clone()),
2815            ],
2816        };
2817
2818        let mut shell_state = ShellState::new();
2819        let exit_code = execute_single_command(&cmd, &mut shell_state);
2820        assert_eq!(exit_code, 0);
2821
2822        // file2 should have the output (last redirection wins)
2823        let content2 = std::fs::read_to_string(&file2).unwrap();
2824        assert!(content2.contains("test"));
2825
2826        // Cleanup
2827        let _ = std::fs::remove_file(&file1);
2828        let _ = std::fs::remove_file(&file2);
2829    }
2830
2831    #[test]
2832    fn test_fd_builtin_with_redirection() {
2833        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2834
2835        // Create unique temp file
2836        use std::time::{SystemTime, UNIX_EPOCH};
2837        let timestamp = SystemTime::now()
2838            .duration_since(UNIX_EPOCH)
2839            .unwrap()
2840            .as_nanos();
2841        let temp_file = format!("/tmp/rush_test_fd_builtin_{}.txt", timestamp);
2842
2843        // Test: Built-in command with fd redirection
2844        let cmd = ShellCommand {
2845            args: vec!["echo".to_string(), "builtin test".to_string()],
2846            redirections: vec![Redirection::Output(temp_file.clone())],
2847            compound: None,
2848        };
2849
2850        let mut shell_state = ShellState::new();
2851        let exit_code = execute_single_command(&cmd, &mut shell_state);
2852        assert_eq!(exit_code, 0);
2853
2854        // Verify output
2855        let content = std::fs::read_to_string(&temp_file).unwrap();
2856        assert!(content.contains("builtin test"));
2857
2858        // Cleanup
2859        let _ = std::fs::remove_file(&temp_file);
2860    }
2861
2862    #[test]
2863    fn test_fd_variable_expansion_in_filename() {
2864        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2865
2866        // Create unique temp file
2867        use std::time::{SystemTime, UNIX_EPOCH};
2868        let timestamp = SystemTime::now()
2869            .duration_since(UNIX_EPOCH)
2870            .unwrap()
2871            .as_nanos();
2872        let temp_file = format!("/tmp/rush_test_fd_var_{}.txt", timestamp);
2873
2874        // Set variable for filename
2875        let mut shell_state = ShellState::new();
2876        shell_state.set_var("OUTFILE", temp_file.clone());
2877
2878        // Test: Variable expansion in redirection filename
2879        let cmd = ShellCommand {
2880            args: vec!["echo".to_string(), "variable test".to_string()],
2881            compound: None,
2882            redirections: vec![Redirection::Output("$OUTFILE".to_string())],
2883        };
2884
2885        let exit_code = execute_single_command(&cmd, &mut shell_state);
2886        assert_eq!(exit_code, 0);
2887
2888        // Verify output
2889        let content = std::fs::read_to_string(&temp_file).unwrap();
2890        assert!(content.contains("variable test"));
2891
2892        // Cleanup
2893        let _ = std::fs::remove_file(&temp_file);
2894    }
2895}