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    // Increment subshell depth in the cloned state
1799    subshell_state.subshell_depth = shell_state.subshell_depth + 1;
1800
1801    // Clone trap handlers for isolation (subshells inherit but don't affect parent)
1802    let parent_traps = shell_state.trap_handlers.lock().unwrap().clone();
1803    subshell_state.trap_handlers = std::sync::Arc::new(std::sync::Mutex::new(parent_traps));
1804
1805    // Execute the body in the isolated state
1806    let exit_code = execute(body, &mut subshell_state);
1807
1808    // Handle exit in subshell: exit should only exit the subshell, not the parent
1809    // The exit_requested flag is isolated to the subshell_state, so it won't affect parent
1810    let final_exit_code = if subshell_state.exit_requested {
1811        // Subshell called exit - use its exit code
1812        subshell_state.exit_code
1813    } else if subshell_state.is_returning() {
1814        // Subshell called return - treat as exit from subshell
1815        // Return in subshell should not propagate to parent function
1816        subshell_state.get_return_value().unwrap_or(exit_code)
1817    } else {
1818        exit_code
1819    };
1820
1821    // Clean up the subshell's file descriptor table to prevent resource leaks
1822    // This ensures any file descriptors opened in the subshell are properly released
1823    subshell_state.fd_table.borrow_mut().clear();
1824
1825    // Restore original directory (in case subshell changed it)
1826    if let Some(dir) = original_dir {
1827        let _ = std::env::set_current_dir(dir);
1828    }
1829
1830    // Update parent's last_exit_code to reflect subshell result
1831    shell_state.last_exit_code = final_exit_code;
1832
1833    // Return the exit code
1834    final_exit_code
1835}
1836
1837/// Execute a compound command with redirections
1838///
1839/// # Arguments
1840/// * `compound_ast` - The compound command AST
1841/// * `shell_state` - The shell state
1842/// * `redirections` - Redirections to apply
1843///
1844/// # Returns
1845/// * Exit code from the compound command
1846fn execute_compound_with_redirections(
1847    compound_ast: &Ast,
1848    shell_state: &mut ShellState,
1849    redirections: &[Redirection],
1850) -> i32 {
1851    match compound_ast {
1852        Ast::Subshell { body } => {
1853            // For subshells with redirections, we need to:
1854            // 1. Set up output capture if there are output redirections
1855            // 2. Execute the subshell
1856            // 3. Apply the redirections to the captured output
1857
1858            // Check if we have output redirections
1859            let has_output_redir = redirections.iter().any(|r| {
1860                matches!(
1861                    r,
1862                    Redirection::Output(_)
1863                        | Redirection::Append(_)
1864                        | Redirection::FdOutput(_, _)
1865                        | Redirection::FdAppend(_, _)
1866                )
1867            });
1868
1869            if has_output_redir {
1870                // Clone state for subshell
1871                let mut subshell_state = shell_state.clone();
1872
1873                // Set up output capture
1874                let capture_buffer = Rc::new(RefCell::new(Vec::new()));
1875                subshell_state.capture_output = Some(capture_buffer.clone());
1876
1877                // Execute subshell
1878                let exit_code = execute(*body.clone(), &mut subshell_state);
1879
1880                // Get captured output
1881                let output = capture_buffer.borrow().clone();
1882
1883                // Apply redirections to output
1884                for redir in redirections {
1885                    match redir {
1886                        Redirection::Output(file) => {
1887                            let expanded_file = expand_variables_in_string(file, shell_state);
1888                            if let Err(e) = std::fs::write(&expanded_file, &output) {
1889                                if shell_state.colors_enabled {
1890                                    eprintln!(
1891                                        "{}Redirection error: {}\x1b[0m",
1892                                        shell_state.color_scheme.error, e
1893                                    );
1894                                } else {
1895                                    eprintln!("Redirection error: {}", e);
1896                                }
1897                                return 1;
1898                            }
1899                        }
1900                        Redirection::Append(file) => {
1901                            let expanded_file = expand_variables_in_string(file, shell_state);
1902                            use std::fs::OpenOptions;
1903                            let mut file_handle = match OpenOptions::new()
1904                                .append(true)
1905                                .create(true)
1906                                .open(&expanded_file)
1907                            {
1908                                Ok(f) => f,
1909                                Err(e) => {
1910                                    if shell_state.colors_enabled {
1911                                        eprintln!(
1912                                            "{}Redirection error: {}\x1b[0m",
1913                                            shell_state.color_scheme.error, e
1914                                        );
1915                                    } else {
1916                                        eprintln!("Redirection error: {}", e);
1917                                    }
1918                                    return 1;
1919                                }
1920                            };
1921                            if let Err(e) = file_handle.write_all(&output) {
1922                                if shell_state.colors_enabled {
1923                                    eprintln!(
1924                                        "{}Redirection error: {}\x1b[0m",
1925                                        shell_state.color_scheme.error, e
1926                                    );
1927                                } else {
1928                                    eprintln!("Redirection error: {}", e);
1929                                }
1930                                return 1;
1931                            }
1932                        }
1933                        _ => {
1934                            // For Phase 2, only support basic output redirections
1935                            // Other redirections are silently ignored for subshells
1936                        }
1937                    }
1938                }
1939
1940                shell_state.last_exit_code = exit_code;
1941                exit_code
1942            } else {
1943                // No output redirections, execute normally
1944                execute_subshell(*body.clone(), shell_state)
1945            }
1946        }
1947        _ => {
1948            eprintln!("Unsupported compound command type");
1949            1
1950        }
1951    }
1952}
1953
1954/// Execute a compound command (subshell) as part of a pipeline
1955///
1956/// # Arguments
1957/// * `compound_ast` - The compound command AST (typically Subshell)
1958/// * `shell_state` - The parent shell state
1959/// * `is_last` - Whether this is the last command in the pipeline
1960/// * `redirections` - Redirections to apply to the compound command
1961///
1962/// # Returns
1963/// * Exit code from the compound command
1964fn execute_compound_in_pipeline(
1965    compound_ast: &Ast,
1966    shell_state: &mut ShellState,
1967    is_first: bool,
1968    is_last: bool,
1969    _redirections: &[Redirection],
1970) -> i32 {
1971    match compound_ast {
1972        Ast::Subshell { body } => {
1973            // Clone state for subshell
1974            let mut subshell_state = shell_state.clone();
1975
1976            // If we are not the first command in pipeline, and since we don't receive input (broken pipe),
1977            // we use stdin_override to force reading from /dev/null, without modifying process-global 0.
1978            // We keep the file handle alive for the duration of the subshell execution.
1979            let _null_file = if !is_first {
1980                if let Ok(f) = File::open("/dev/null") {
1981                    subshell_state.stdin_override = Some(f.as_raw_fd());
1982                    Some(f)
1983                } else {
1984                    None
1985                }
1986            } else {
1987                None
1988            };
1989
1990            // Execute subshell with appropriate stdout capture
1991            let exit_code = if !is_last || shell_state.capture_output.is_some() {
1992                // Need to capture subshell output
1993                let capture_buffer = Rc::new(RefCell::new(Vec::new()));
1994                subshell_state.capture_output = Some(capture_buffer.clone());
1995
1996                // Execute subshell
1997                let code = execute(*body.clone(), &mut subshell_state);
1998
1999                // Transfer captured output to parent's capture buffer
2000                if let Some(ref parent_capture) = shell_state.capture_output {
2001                    let captured = capture_buffer.borrow().clone();
2002                    parent_capture.borrow_mut().extend_from_slice(&captured);
2003                }
2004
2005                // Update parent's last_exit_code
2006                shell_state.last_exit_code = code;
2007
2008                code
2009            } else {
2010                // Last command, no capture needed
2011                let code = execute(*body.clone(), &mut subshell_state);
2012                shell_state.last_exit_code = code;
2013                code
2014            };
2015
2016            exit_code
2017        }
2018        _ => {
2019            // Other compound commands not yet supported
2020            eprintln!("Unsupported compound command in pipeline");
2021            1
2022        }
2023    }
2024}
2025
2026#[cfg(test)]
2027mod tests {
2028    use super::*;
2029    use std::sync::Mutex;
2030
2031    // Mutex to serialize tests that modify environment variables or create files
2032    static ENV_LOCK: Mutex<()> = Mutex::new(());
2033
2034    #[test]
2035    fn test_execute_single_command_builtin() {
2036        let cmd = ShellCommand {
2037            args: vec!["true".to_string()],
2038            redirections: Vec::new(),
2039            compound: None,
2040        };
2041        let mut shell_state = ShellState::new();
2042        let exit_code = execute_single_command(&cmd, &mut shell_state);
2043        assert_eq!(exit_code, 0);
2044    }
2045
2046    // For external commands, test with a command that exists
2047    #[test]
2048    fn test_execute_single_command_external() {
2049        let cmd = ShellCommand {
2050            args: vec!["true".to_string()], // Assume true exists
2051            redirections: Vec::new(),
2052            compound: None,
2053        };
2054        let mut shell_state = ShellState::new();
2055        let exit_code = execute_single_command(&cmd, &mut shell_state);
2056        assert_eq!(exit_code, 0);
2057    }
2058
2059    #[test]
2060    fn test_execute_single_command_external_nonexistent() {
2061        let cmd = ShellCommand {
2062            args: vec!["nonexistent_command".to_string()],
2063            redirections: Vec::new(),
2064            compound: None,
2065        };
2066        let mut shell_state = ShellState::new();
2067        let exit_code = execute_single_command(&cmd, &mut shell_state);
2068        assert_eq!(exit_code, 1); // Command not found
2069    }
2070
2071    #[test]
2072    fn test_execute_pipeline() {
2073        let commands = vec![
2074            ShellCommand {
2075                args: vec!["printf".to_string(), "hello".to_string()],
2076                redirections: Vec::new(),
2077                compound: None,
2078            },
2079            ShellCommand {
2080                args: vec!["cat".to_string()], // cat reads from stdin
2081                redirections: Vec::new(),
2082                compound: None,
2083            },
2084        ];
2085        let mut shell_state = ShellState::new();
2086        let exit_code = execute_pipeline(&commands, &mut shell_state);
2087        assert_eq!(exit_code, 0);
2088    }
2089
2090    #[test]
2091    fn test_execute_empty_pipeline() {
2092        let commands = vec![];
2093        let mut shell_state = ShellState::new();
2094        let exit_code = execute(Ast::Pipeline(commands), &mut shell_state);
2095        assert_eq!(exit_code, 0);
2096    }
2097
2098    #[test]
2099    fn test_execute_single_command() {
2100        let ast = Ast::Pipeline(vec![ShellCommand {
2101            args: vec!["true".to_string()],
2102            redirections: Vec::new(),
2103            compound: None,
2104        }]);
2105        let mut shell_state = ShellState::new();
2106        let exit_code = execute(ast, &mut shell_state);
2107        assert_eq!(exit_code, 0);
2108    }
2109
2110    #[test]
2111    fn test_execute_function_definition() {
2112        let ast = Ast::FunctionDefinition {
2113            name: "test_func".to_string(),
2114            body: Box::new(Ast::Pipeline(vec![ShellCommand {
2115                args: vec!["echo".to_string(), "hello".to_string()],
2116                redirections: Vec::new(),
2117                compound: None,
2118            }])),
2119        };
2120        let mut shell_state = ShellState::new();
2121        let exit_code = execute(ast, &mut shell_state);
2122        assert_eq!(exit_code, 0);
2123
2124        // Check that function was stored
2125        assert!(shell_state.get_function("test_func").is_some());
2126    }
2127
2128    #[test]
2129    fn test_execute_function_call() {
2130        // First define a function
2131        let mut shell_state = ShellState::new();
2132        shell_state.define_function(
2133            "test_func".to_string(),
2134            Ast::Pipeline(vec![ShellCommand {
2135                args: vec!["echo".to_string(), "hello".to_string()],
2136                redirections: Vec::new(),
2137                compound: None,
2138            }]),
2139        );
2140
2141        // Now call the function
2142        let ast = Ast::FunctionCall {
2143            name: "test_func".to_string(),
2144            args: vec![],
2145        };
2146        let exit_code = execute(ast, &mut shell_state);
2147        assert_eq!(exit_code, 0);
2148    }
2149
2150    #[test]
2151    fn test_execute_function_call_with_args() {
2152        // First define a function that uses arguments
2153        let mut shell_state = ShellState::new();
2154        shell_state.define_function(
2155            "test_func".to_string(),
2156            Ast::Pipeline(vec![ShellCommand {
2157                args: vec!["echo".to_string(), "arg1".to_string()],
2158                redirections: Vec::new(),
2159                compound: None,
2160            }]),
2161        );
2162
2163        // Now call the function with arguments
2164        let ast = Ast::FunctionCall {
2165            name: "test_func".to_string(),
2166            args: vec!["hello".to_string()],
2167        };
2168        let exit_code = execute(ast, &mut shell_state);
2169        assert_eq!(exit_code, 0);
2170    }
2171
2172    #[test]
2173    fn test_execute_nonexistent_function() {
2174        let mut shell_state = ShellState::new();
2175        let ast = Ast::FunctionCall {
2176            name: "nonexistent".to_string(),
2177            args: vec![],
2178        };
2179        let exit_code = execute(ast, &mut shell_state);
2180        assert_eq!(exit_code, 1); // Should return error code
2181    }
2182
2183    #[test]
2184    fn test_execute_function_integration() {
2185        // Test full integration: define function, then call it
2186        let mut shell_state = ShellState::new();
2187
2188        // First define a function
2189        let define_ast = Ast::FunctionDefinition {
2190            name: "hello".to_string(),
2191            body: Box::new(Ast::Pipeline(vec![ShellCommand {
2192                args: vec!["printf".to_string(), "Hello from function".to_string()],
2193                redirections: Vec::new(),
2194                compound: None,
2195            }])),
2196        };
2197        let exit_code = execute(define_ast, &mut shell_state);
2198        assert_eq!(exit_code, 0);
2199
2200        // Now call the function
2201        let call_ast = Ast::FunctionCall {
2202            name: "hello".to_string(),
2203            args: vec![],
2204        };
2205        let exit_code = execute(call_ast, &mut shell_state);
2206        assert_eq!(exit_code, 0);
2207    }
2208
2209    #[test]
2210    fn test_execute_function_with_local_variables() {
2211        let mut shell_state = ShellState::new();
2212
2213        // Set a global variable
2214        shell_state.set_var("global_var", "global_value".to_string());
2215
2216        // Define a function that uses local variables
2217        let define_ast = Ast::FunctionDefinition {
2218            name: "test_func".to_string(),
2219            body: Box::new(Ast::Sequence(vec![
2220                Ast::LocalAssignment {
2221                    var: "local_var".to_string(),
2222                    value: "local_value".to_string(),
2223                },
2224                Ast::Assignment {
2225                    var: "global_var".to_string(),
2226                    value: "modified_in_function".to_string(),
2227                },
2228                Ast::Pipeline(vec![ShellCommand {
2229                    args: vec!["printf".to_string(), "success".to_string()],
2230                    redirections: Vec::new(),
2231                    compound: None,
2232                }]),
2233            ])),
2234        };
2235        let exit_code = execute(define_ast, &mut shell_state);
2236        assert_eq!(exit_code, 0);
2237
2238        // Global variable should not be modified during function definition
2239        assert_eq!(
2240            shell_state.get_var("global_var"),
2241            Some("global_value".to_string())
2242        );
2243
2244        // Call the function
2245        let call_ast = Ast::FunctionCall {
2246            name: "test_func".to_string(),
2247            args: vec![],
2248        };
2249        let exit_code = execute(call_ast, &mut shell_state);
2250        assert_eq!(exit_code, 0);
2251
2252        // After function call, global variable should be modified since function assignments affect global scope
2253        assert_eq!(
2254            shell_state.get_var("global_var"),
2255            Some("modified_in_function".to_string())
2256        );
2257    }
2258
2259    #[test]
2260    fn test_execute_nested_function_calls() {
2261        let mut shell_state = ShellState::new();
2262
2263        // Set global variable
2264        shell_state.set_var("global_var", "global".to_string());
2265
2266        // Define outer function
2267        let outer_func = Ast::FunctionDefinition {
2268            name: "outer".to_string(),
2269            body: Box::new(Ast::Sequence(vec![
2270                Ast::Assignment {
2271                    var: "global_var".to_string(),
2272                    value: "outer_modified".to_string(),
2273                },
2274                Ast::FunctionCall {
2275                    name: "inner".to_string(),
2276                    args: vec![],
2277                },
2278                Ast::Pipeline(vec![ShellCommand {
2279                    args: vec!["printf".to_string(), "outer_done".to_string()],
2280                    redirections: Vec::new(),
2281                    compound: None,
2282                }]),
2283            ])),
2284        };
2285
2286        // Define inner function
2287        let inner_func = Ast::FunctionDefinition {
2288            name: "inner".to_string(),
2289            body: Box::new(Ast::Sequence(vec![
2290                Ast::Assignment {
2291                    var: "global_var".to_string(),
2292                    value: "inner_modified".to_string(),
2293                },
2294                Ast::Pipeline(vec![ShellCommand {
2295                    args: vec!["printf".to_string(), "inner_done".to_string()],
2296                    redirections: Vec::new(),
2297                    compound: None,
2298                }]),
2299            ])),
2300        };
2301
2302        // Define both functions
2303        execute(outer_func, &mut shell_state);
2304        execute(inner_func, &mut shell_state);
2305
2306        // Set initial global value
2307        shell_state.set_var("global_var", "initial".to_string());
2308
2309        // Call outer function (which calls inner function)
2310        let call_ast = Ast::FunctionCall {
2311            name: "outer".to_string(),
2312            args: vec![],
2313        };
2314        let exit_code = execute(call_ast, &mut shell_state);
2315        assert_eq!(exit_code, 0);
2316
2317        // After nested function calls, global variable should be modified by inner function
2318        // (bash behavior: function variable assignments affect global scope)
2319        assert_eq!(
2320            shell_state.get_var("global_var"),
2321            Some("inner_modified".to_string())
2322        );
2323    }
2324
2325    #[test]
2326    fn test_here_string_execution() {
2327        // Test here-string redirection with a simple command
2328        let cmd = ShellCommand {
2329            args: vec!["cat".to_string()],
2330            redirections: Vec::new(),
2331            compound: None,
2332            // TODO: Update test for new redirection system
2333        };
2334
2335        // Note: This test would require mocking stdin to provide the here-string content
2336        // For now, we'll just verify the command structure is parsed correctly
2337        assert_eq!(cmd.args, vec!["cat"]);
2338        // assert_eq!(cmd.here_string_content, Some("hello world".to_string()));
2339    }
2340
2341    #[test]
2342    fn test_here_document_execution() {
2343        // Test here-document redirection with a simple command
2344        let cmd = ShellCommand {
2345            args: vec!["cat".to_string()],
2346            redirections: Vec::new(),
2347            compound: None,
2348            // TODO: Update test for new redirection system
2349        };
2350
2351        // Note: This test would require mocking stdin to provide the here-document content
2352        // For now, we'll just verify the command structure is parsed correctly
2353        assert_eq!(cmd.args, vec!["cat"]);
2354        // assert_eq!(cmd.here_doc_delimiter, Some("EOF".to_string()));
2355    }
2356
2357    #[test]
2358    fn test_here_document_with_variable_expansion() {
2359        // Test that variables are expanded in here-document content
2360        let mut shell_state = ShellState::new();
2361        shell_state.set_var("PWD", "/test/path".to_string());
2362
2363        // Simulate here-doc content with variable
2364        let content = "Working dir: $PWD";
2365        let expanded = expand_variables_in_string(content, &mut shell_state);
2366
2367        assert_eq!(expanded, "Working dir: /test/path");
2368    }
2369
2370    #[test]
2371    fn test_here_document_with_command_substitution_builtin() {
2372        // Test that builtin command substitutions work in here-document content
2373        let mut shell_state = ShellState::new();
2374        shell_state.set_var("PWD", "/test/dir".to_string());
2375
2376        // Simulate here-doc content with pwd builtin command substitution
2377        let content = "Current directory: `pwd`";
2378        let expanded = expand_variables_in_string(content, &mut shell_state);
2379
2380        // The pwd builtin should be executed and expanded
2381        assert!(expanded.contains("Current directory: "));
2382    }
2383
2384    // ========================================================================
2385    // File Descriptor Integration Tests
2386    // ========================================================================
2387
2388    #[test]
2389    fn test_fd_output_redirection() {
2390        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2391
2392        // Create unique temp file
2393        use std::time::{SystemTime, UNIX_EPOCH};
2394        let timestamp = SystemTime::now()
2395            .duration_since(UNIX_EPOCH)
2396            .unwrap()
2397            .as_nanos();
2398        let temp_file = format!("/tmp/rush_test_fd_out_{}.txt", timestamp);
2399
2400        // Test: echo "error" 2>errors.txt
2401        let cmd = ShellCommand {
2402            args: vec![
2403                "sh".to_string(),
2404                "-c".to_string(),
2405                "echo error >&2".to_string(),
2406            ],
2407            redirections: vec![Redirection::FdOutput(2, temp_file.clone())],
2408            compound: None,
2409        };
2410
2411        let mut shell_state = ShellState::new();
2412        let exit_code = execute_single_command(&cmd, &mut shell_state);
2413        assert_eq!(exit_code, 0);
2414
2415        // Verify file was created and contains the error message
2416        let content = std::fs::read_to_string(&temp_file).unwrap();
2417        assert_eq!(content.trim(), "error");
2418
2419        // Cleanup
2420        let _ = std::fs::remove_file(&temp_file);
2421    }
2422
2423    #[test]
2424    fn test_fd_input_redirection() {
2425        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2426
2427        // Create unique temp file with content
2428        use std::time::{SystemTime, UNIX_EPOCH};
2429        let timestamp = SystemTime::now()
2430            .duration_since(UNIX_EPOCH)
2431            .unwrap()
2432            .as_nanos();
2433        let temp_file = format!("/tmp/rush_test_fd_in_{}.txt", timestamp);
2434
2435        std::fs::write(&temp_file, "test input\n").unwrap();
2436        std::thread::sleep(std::time::Duration::from_millis(10));
2437
2438        // Test: cat 3<input.txt (reading from fd 3)
2439        // Note: This tests that fd 3 is opened for reading
2440        let cmd = ShellCommand {
2441            args: vec!["cat".to_string()],
2442            compound: None,
2443            redirections: vec![
2444                Redirection::FdInput(3, temp_file.clone()),
2445                Redirection::Input(temp_file.clone()),
2446            ],
2447        };
2448
2449        let mut shell_state = ShellState::new();
2450        let exit_code = execute_single_command(&cmd, &mut shell_state);
2451        assert_eq!(exit_code, 0);
2452
2453        // Cleanup
2454        let _ = std::fs::remove_file(&temp_file);
2455    }
2456
2457    #[test]
2458    fn test_fd_append_redirection() {
2459        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2460
2461        // Create unique temp file with initial content
2462        use std::time::{SystemTime, UNIX_EPOCH};
2463        let timestamp = SystemTime::now()
2464            .duration_since(UNIX_EPOCH)
2465            .unwrap()
2466            .as_nanos();
2467        let temp_file = format!("/tmp/rush_test_fd_append_{}.txt", timestamp);
2468
2469        std::fs::write(&temp_file, "first line\n").unwrap();
2470        std::thread::sleep(std::time::Duration::from_millis(10));
2471
2472        // Test: echo "more" 2>>errors.txt
2473        let cmd = ShellCommand {
2474            args: vec![
2475                "sh".to_string(),
2476                "-c".to_string(),
2477                "echo second line >&2".to_string(),
2478            ],
2479            redirections: vec![Redirection::FdAppend(2, temp_file.clone())],
2480            compound: None,
2481        };
2482
2483        let mut shell_state = ShellState::new();
2484        let exit_code = execute_single_command(&cmd, &mut shell_state);
2485        assert_eq!(exit_code, 0);
2486
2487        // Verify file contains both lines
2488        let content = std::fs::read_to_string(&temp_file).unwrap();
2489        assert!(content.contains("first line"));
2490        assert!(content.contains("second line"));
2491
2492        // Cleanup
2493        let _ = std::fs::remove_file(&temp_file);
2494    }
2495
2496    #[test]
2497    fn test_fd_duplication_stderr_to_stdout() {
2498        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2499
2500        // Create unique temp file
2501        use std::time::{SystemTime, UNIX_EPOCH};
2502        let timestamp = SystemTime::now()
2503            .duration_since(UNIX_EPOCH)
2504            .unwrap()
2505            .as_nanos();
2506        let temp_file = format!("/tmp/rush_test_fd_dup_{}.txt", timestamp);
2507
2508        // Test: command 2>&1 >output.txt
2509        // Note: For external commands, fd duplication is handled by the shell
2510        // We test that the command executes successfully with the redirection
2511        let cmd = ShellCommand {
2512            args: vec![
2513                "sh".to_string(),
2514                "-c".to_string(),
2515                "echo test; echo error >&2".to_string(),
2516            ],
2517            compound: None,
2518            redirections: vec![Redirection::Output(temp_file.clone())],
2519        };
2520
2521        let mut shell_state = ShellState::new();
2522        let exit_code = execute_single_command(&cmd, &mut shell_state);
2523        assert_eq!(exit_code, 0);
2524
2525        // Verify file was created and contains output
2526        assert!(std::path::Path::new(&temp_file).exists());
2527        let content = std::fs::read_to_string(&temp_file).unwrap();
2528        assert!(content.contains("test"));
2529
2530        // Cleanup
2531        let _ = std::fs::remove_file(&temp_file);
2532    }
2533
2534    #[test]
2535    fn test_fd_close() {
2536        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2537
2538        // Test: command 2>&- (closes stderr)
2539        let cmd = ShellCommand {
2540            args: vec!["sh".to_string(), "-c".to_string(), "echo test".to_string()],
2541            redirections: vec![Redirection::FdClose(2)],
2542            compound: None,
2543        };
2544
2545        let mut shell_state = ShellState::new();
2546        let exit_code = execute_single_command(&cmd, &mut shell_state);
2547        assert_eq!(exit_code, 0);
2548
2549        // Verify fd 2 is closed in the fd table
2550        assert!(shell_state.fd_table.borrow().is_closed(2));
2551    }
2552
2553    #[test]
2554    fn test_fd_read_write() {
2555        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2556
2557        // Create unique temp file
2558        use std::time::{SystemTime, UNIX_EPOCH};
2559        let timestamp = SystemTime::now()
2560            .duration_since(UNIX_EPOCH)
2561            .unwrap()
2562            .as_nanos();
2563        let temp_file = format!("/tmp/rush_test_fd_rw_{}.txt", timestamp);
2564
2565        std::fs::write(&temp_file, "initial content\n").unwrap();
2566        std::thread::sleep(std::time::Duration::from_millis(10));
2567
2568        // Test: 3<>file.txt (opens fd 3 for read/write)
2569        let cmd = ShellCommand {
2570            args: vec!["cat".to_string()],
2571            compound: None,
2572            redirections: vec![
2573                Redirection::FdInputOutput(3, temp_file.clone()),
2574                Redirection::Input(temp_file.clone()),
2575            ],
2576        };
2577
2578        let mut shell_state = ShellState::new();
2579        let exit_code = execute_single_command(&cmd, &mut shell_state);
2580        assert_eq!(exit_code, 0);
2581
2582        // Cleanup
2583        let _ = std::fs::remove_file(&temp_file);
2584    }
2585
2586    #[test]
2587    fn test_multiple_fd_redirections() {
2588        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2589
2590        // Create unique temp files
2591        use std::time::{SystemTime, UNIX_EPOCH};
2592        let timestamp = SystemTime::now()
2593            .duration_since(UNIX_EPOCH)
2594            .unwrap()
2595            .as_nanos();
2596        let out_file = format!("/tmp/rush_test_fd_multi_out_{}.txt", timestamp);
2597        let err_file = format!("/tmp/rush_test_fd_multi_err_{}.txt", timestamp);
2598
2599        // Test: command 2>err.txt 1>out.txt
2600        let cmd = ShellCommand {
2601            args: vec![
2602                "sh".to_string(),
2603                "-c".to_string(),
2604                "echo stdout; echo stderr >&2".to_string(),
2605            ],
2606            redirections: vec![
2607                Redirection::FdOutput(2, err_file.clone()),
2608                Redirection::Output(out_file.clone()),
2609            ],
2610            compound: None,
2611        };
2612
2613        let mut shell_state = ShellState::new();
2614        let exit_code = execute_single_command(&cmd, &mut shell_state);
2615        assert_eq!(exit_code, 0);
2616
2617        // Verify both files were created
2618        assert!(std::path::Path::new(&out_file).exists());
2619        assert!(std::path::Path::new(&err_file).exists());
2620
2621        // Verify content
2622        let out_content = std::fs::read_to_string(&out_file).unwrap();
2623        let err_content = std::fs::read_to_string(&err_file).unwrap();
2624        assert!(out_content.contains("stdout"));
2625        assert!(err_content.contains("stderr"));
2626
2627        // Cleanup
2628        let _ = std::fs::remove_file(&out_file);
2629        let _ = std::fs::remove_file(&err_file);
2630    }
2631
2632    #[test]
2633    fn test_fd_swap_pattern() {
2634        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2635
2636        // Create unique temp files
2637        use std::time::{SystemTime, UNIX_EPOCH};
2638        let timestamp = SystemTime::now()
2639            .duration_since(UNIX_EPOCH)
2640            .unwrap()
2641            .as_nanos();
2642        let temp_file = format!("/tmp/rush_test_fd_swap_{}.txt", timestamp);
2643
2644        // Test fd operations: open fd 3, then close it
2645        // This tests the fd table operations
2646        let cmd = ShellCommand {
2647            args: vec!["sh".to_string(), "-c".to_string(), "echo test".to_string()],
2648            redirections: vec![
2649                Redirection::FdOutput(3, temp_file.clone()), // Open fd 3 for writing
2650                Redirection::FdClose(3),                     // Close fd 3
2651                Redirection::Output(temp_file.clone()),      // Write to stdout
2652            ],
2653            compound: None,
2654        };
2655
2656        let mut shell_state = ShellState::new();
2657        let exit_code = execute_single_command(&cmd, &mut shell_state);
2658        assert_eq!(exit_code, 0);
2659
2660        // Verify fd 3 is closed after the operations
2661        assert!(shell_state.fd_table.borrow().is_closed(3));
2662
2663        // Cleanup
2664        let _ = std::fs::remove_file(&temp_file);
2665    }
2666
2667    #[test]
2668    fn test_fd_redirection_with_pipes() {
2669        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2670
2671        // Create unique temp file
2672        use std::time::{SystemTime, UNIX_EPOCH};
2673        let timestamp = SystemTime::now()
2674            .duration_since(UNIX_EPOCH)
2675            .unwrap()
2676            .as_nanos();
2677        let temp_file = format!("/tmp/rush_test_fd_pipe_{}.txt", timestamp);
2678
2679        // Test: cmd1 | cmd2 >output.txt
2680        // This tests redirections in pipelines
2681        let commands = vec![
2682            ShellCommand {
2683                args: vec!["echo".to_string(), "piped output".to_string()],
2684                redirections: vec![],
2685                compound: None,
2686            },
2687            ShellCommand {
2688                args: vec!["cat".to_string()],
2689                compound: None,
2690                redirections: vec![Redirection::Output(temp_file.clone())],
2691            },
2692        ];
2693
2694        let mut shell_state = ShellState::new();
2695        let exit_code = execute_pipeline(&commands, &mut shell_state);
2696        assert_eq!(exit_code, 0);
2697
2698        // Verify output file contains the piped content
2699        let content = std::fs::read_to_string(&temp_file).unwrap();
2700        assert!(content.contains("piped output"));
2701
2702        // Cleanup
2703        let _ = std::fs::remove_file(&temp_file);
2704    }
2705
2706    #[test]
2707    fn test_fd_error_invalid_fd_number() {
2708        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2709
2710        // Create unique temp file
2711        use std::time::{SystemTime, UNIX_EPOCH};
2712        let timestamp = SystemTime::now()
2713            .duration_since(UNIX_EPOCH)
2714            .unwrap()
2715            .as_nanos();
2716        let temp_file = format!("/tmp/rush_test_fd_invalid_{}.txt", timestamp);
2717
2718        // Test: Invalid fd number (>1024)
2719        let cmd = ShellCommand {
2720            args: vec!["echo".to_string(), "test".to_string()],
2721            compound: None,
2722            redirections: vec![Redirection::FdOutput(1025, temp_file.clone())],
2723        };
2724
2725        let mut shell_state = ShellState::new();
2726        let exit_code = execute_single_command(&cmd, &mut shell_state);
2727
2728        // Should fail with error
2729        assert_eq!(exit_code, 1);
2730
2731        // Cleanup (file may not exist)
2732        let _ = std::fs::remove_file(&temp_file);
2733    }
2734
2735    #[test]
2736    fn test_fd_error_duplicate_closed_fd() {
2737        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2738
2739        // Test: Attempting to duplicate a closed fd
2740        let cmd = ShellCommand {
2741            args: vec!["echo".to_string(), "test".to_string()],
2742            compound: None,
2743            redirections: vec![
2744                Redirection::FdClose(3),
2745                Redirection::FdDuplicate(2, 3), // Try to duplicate closed fd 3
2746            ],
2747        };
2748
2749        let mut shell_state = ShellState::new();
2750        let exit_code = execute_single_command(&cmd, &mut shell_state);
2751
2752        // Should fail with error
2753        assert_eq!(exit_code, 1);
2754    }
2755
2756    #[test]
2757    fn test_fd_error_file_permission() {
2758        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2759
2760        // Test: Attempting to write to a read-only location
2761        let cmd = ShellCommand {
2762            args: vec!["echo".to_string(), "test".to_string()],
2763            redirections: vec![Redirection::FdOutput(2, "/proc/version".to_string())],
2764            compound: None,
2765        };
2766
2767        let mut shell_state = ShellState::new();
2768        let exit_code = execute_single_command(&cmd, &mut shell_state);
2769
2770        // Should fail with permission error
2771        assert_eq!(exit_code, 1);
2772    }
2773
2774    #[test]
2775    fn test_fd_redirection_order() {
2776        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2777
2778        // Create unique temp files
2779        use std::time::{SystemTime, UNIX_EPOCH};
2780        let timestamp = SystemTime::now()
2781            .duration_since(UNIX_EPOCH)
2782            .unwrap()
2783            .as_nanos();
2784        let file1 = format!("/tmp/rush_test_fd_order1_{}.txt", timestamp);
2785        let file2 = format!("/tmp/rush_test_fd_order2_{}.txt", timestamp);
2786
2787        // Test: Redirections are processed left-to-right
2788        // 1>file1 1>file2 should write to file2
2789        let cmd = ShellCommand {
2790            args: vec!["echo".to_string(), "test".to_string()],
2791            compound: None,
2792            redirections: vec![
2793                Redirection::Output(file1.clone()),
2794                Redirection::Output(file2.clone()),
2795            ],
2796        };
2797
2798        let mut shell_state = ShellState::new();
2799        let exit_code = execute_single_command(&cmd, &mut shell_state);
2800        assert_eq!(exit_code, 0);
2801
2802        // file2 should have the output (last redirection wins)
2803        let content2 = std::fs::read_to_string(&file2).unwrap();
2804        assert!(content2.contains("test"));
2805
2806        // Cleanup
2807        let _ = std::fs::remove_file(&file1);
2808        let _ = std::fs::remove_file(&file2);
2809    }
2810
2811    #[test]
2812    fn test_fd_builtin_with_redirection() {
2813        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2814
2815        // Create unique temp file
2816        use std::time::{SystemTime, UNIX_EPOCH};
2817        let timestamp = SystemTime::now()
2818            .duration_since(UNIX_EPOCH)
2819            .unwrap()
2820            .as_nanos();
2821        let temp_file = format!("/tmp/rush_test_fd_builtin_{}.txt", timestamp);
2822
2823        // Test: Built-in command with fd redirection
2824        let cmd = ShellCommand {
2825            args: vec!["echo".to_string(), "builtin test".to_string()],
2826            redirections: vec![Redirection::Output(temp_file.clone())],
2827            compound: None,
2828        };
2829
2830        let mut shell_state = ShellState::new();
2831        let exit_code = execute_single_command(&cmd, &mut shell_state);
2832        assert_eq!(exit_code, 0);
2833
2834        // Verify output
2835        let content = std::fs::read_to_string(&temp_file).unwrap();
2836        assert!(content.contains("builtin test"));
2837
2838        // Cleanup
2839        let _ = std::fs::remove_file(&temp_file);
2840    }
2841
2842    #[test]
2843    fn test_fd_variable_expansion_in_filename() {
2844        let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2845
2846        // Create unique temp file
2847        use std::time::{SystemTime, UNIX_EPOCH};
2848        let timestamp = SystemTime::now()
2849            .duration_since(UNIX_EPOCH)
2850            .unwrap()
2851            .as_nanos();
2852        let temp_file = format!("/tmp/rush_test_fd_var_{}.txt", timestamp);
2853
2854        // Set variable for filename
2855        let mut shell_state = ShellState::new();
2856        shell_state.set_var("OUTFILE", temp_file.clone());
2857
2858        // Test: Variable expansion in redirection filename
2859        let cmd = ShellCommand {
2860            args: vec!["echo".to_string(), "variable test".to_string()],
2861            compound: None,
2862            redirections: vec![Redirection::Output("$OUTFILE".to_string())],
2863        };
2864
2865        let exit_code = execute_single_command(&cmd, &mut shell_state);
2866        assert_eq!(exit_code, 0);
2867
2868        // Verify output
2869        let content = std::fs::read_to_string(&temp_file).unwrap();
2870        assert!(content.contains("variable test"));
2871
2872        // Cleanup
2873        let _ = std::fs::remove_file(&temp_file);
2874    }
2875}