rush_sh/
executor.rs

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