rush_sh/
executor.rs

1use std::cell::RefCell;
2use std::fs::File;
3use std::io::{BufRead, BufReader, pipe};
4use std::process::{Command, Stdio};
5use std::rc::Rc;
6
7use super::parser::{Ast, ShellCommand};
8use super::state::ShellState;
9
10/// Execute a command and capture its output as a string
11/// This is used for command substitution $(...)
12fn execute_and_capture_output(ast: Ast, shell_state: &mut ShellState) -> Result<String, String> {
13    // Create a pipe to capture stdout
14    let (reader, writer) = pipe().map_err(|e| format!("Failed to create pipe: {}", e))?;
15
16    // We need to capture the output, so we'll redirect stdout to our pipe
17    // For builtins, we can pass the writer directly
18    // For external commands, we need to handle them specially
19
20    match &ast {
21        Ast::Pipeline(commands) => {
22            // Handle both single commands and multi-command pipelines
23            if commands.is_empty() {
24                return Ok(String::new());
25            }
26
27            if commands.len() == 1 {
28                // Single command - use the existing optimized path
29                let cmd = &commands[0];
30                if cmd.args.is_empty() {
31                    return Ok(String::new());
32                }
33
34                // Expand variables and wildcards
35                let var_expanded_args = expand_variables_in_args(&cmd.args, shell_state);
36                let expanded_args = expand_wildcards(&var_expanded_args)
37                    .map_err(|e| format!("Wildcard expansion failed: {}", e))?;
38
39                if expanded_args.is_empty() {
40                    return Ok(String::new());
41                }
42
43                // Check if it's a function call
44                if shell_state.get_function(&expanded_args[0]).is_some() {
45                    // Save previous capture state (for nested command substitutions)
46                    let previous_capture = shell_state.capture_output.clone();
47
48                    // Enable output capture mode
49                    let capture_buffer = Rc::new(RefCell::new(Vec::new()));
50                    shell_state.capture_output = Some(capture_buffer.clone());
51
52                    // Create a FunctionCall AST and execute it
53                    let function_call_ast = Ast::FunctionCall {
54                        name: expanded_args[0].clone(),
55                        args: expanded_args[1..].to_vec(),
56                    };
57
58                    let exit_code = execute(function_call_ast, shell_state);
59
60                    // Retrieve captured output
61                    let captured = capture_buffer.borrow().clone();
62                    let output = String::from_utf8_lossy(&captured).trim_end().to_string();
63
64                    // Restore previous capture state
65                    shell_state.capture_output = previous_capture;
66
67                    if exit_code == 0 {
68                        Ok(output)
69                    } else {
70                        Err(format!("Function failed with exit code {}", exit_code))
71                    }
72                } else if crate::builtins::is_builtin(&expanded_args[0]) {
73                    let temp_cmd = ShellCommand {
74                        args: expanded_args,
75                        input: cmd.input.clone(),
76                        output: None, // We're capturing output
77                        append: None,
78                        here_doc_delimiter: None,
79                        here_doc_quoted: false,
80                        here_string_content: None,
81                    };
82
83                    // Execute builtin with our writer
84                    let exit_code = crate::builtins::execute_builtin(
85                        &temp_cmd,
86                        shell_state,
87                        Some(Box::new(writer)),
88                    );
89
90                    // Read the captured output
91                    drop(temp_cmd); // Ensure writer is dropped
92                    let mut output = String::new();
93                    use std::io::Read;
94                    let mut reader = reader;
95                    reader
96                        .read_to_string(&mut output)
97                        .map_err(|e| format!("Failed to read output: {}", e))?;
98
99                    if exit_code == 0 {
100                        Ok(output.trim_end().to_string())
101                    } else {
102                        Err(format!("Command failed with exit code {}", exit_code))
103                    }
104                } else {
105                    // External command - execute with output capture
106                    drop(writer); // Close writer end before spawning
107
108                    let mut command = Command::new(&expanded_args[0]);
109                    command.args(&expanded_args[1..]);
110                    command.stdout(Stdio::piped());
111                    command.stderr(Stdio::null()); // Suppress stderr for command substitution
112
113                    // Set environment
114                    let child_env = shell_state.get_env_for_child();
115                    command.env_clear();
116                    for (key, value) in child_env {
117                        command.env(key, value);
118                    }
119
120                    let output = command
121                        .output()
122                        .map_err(|e| format!("Failed to execute command: {}", e))?;
123
124                    if output.status.success() {
125                        Ok(String::from_utf8_lossy(&output.stdout)
126                            .trim_end()
127                            .to_string())
128                    } else {
129                        Err(format!(
130                            "Command failed with exit code {}",
131                            output.status.code().unwrap_or(1)
132                        ))
133                    }
134                }
135            } else {
136                // Multi-command pipeline - execute the entire pipeline and capture output
137                drop(writer); // Close writer end before executing pipeline
138
139                // Save previous capture state (for nested command substitutions)
140                let previous_capture = shell_state.capture_output.clone();
141
142                // Enable output capture mode
143                let capture_buffer = Rc::new(RefCell::new(Vec::new()));
144                shell_state.capture_output = Some(capture_buffer.clone());
145
146                // Execute the pipeline
147                let exit_code = execute_pipeline(commands, shell_state);
148
149                // Retrieve captured output
150                let captured = capture_buffer.borrow().clone();
151                let output = String::from_utf8_lossy(&captured).trim_end().to_string();
152
153                // Restore previous capture state
154                shell_state.capture_output = previous_capture;
155
156                if exit_code == 0 {
157                    Ok(output)
158                } else {
159                    Err(format!("Pipeline failed with exit code {}", exit_code))
160                }
161            }
162        }
163        _ => {
164            // For other AST nodes (sequences, etc.), we need special handling
165            drop(writer);
166
167            // Save previous capture state
168            let previous_capture = shell_state.capture_output.clone();
169
170            // Enable output capture mode
171            let capture_buffer = Rc::new(RefCell::new(Vec::new()));
172            shell_state.capture_output = Some(capture_buffer.clone());
173
174            // Execute the AST
175            let exit_code = execute(ast, shell_state);
176
177            // Retrieve captured output
178            let captured = capture_buffer.borrow().clone();
179            let output = String::from_utf8_lossy(&captured).trim_end().to_string();
180
181            // Restore previous capture state
182            shell_state.capture_output = previous_capture;
183
184            if exit_code == 0 {
185                Ok(output)
186            } else {
187                Err(format!("Command failed with exit code {}", exit_code))
188            }
189        }
190    }
191}
192
193fn expand_variables_in_args(args: &[String], shell_state: &mut ShellState) -> Vec<String> {
194    let mut expanded_args = Vec::new();
195
196    for arg in args {
197        // Expand variables within the argument string
198        let expanded_arg = expand_variables_in_string(arg, shell_state);
199        expanded_args.push(expanded_arg);
200    }
201
202    expanded_args
203}
204
205pub fn expand_variables_in_string(input: &str, shell_state: &mut ShellState) -> String {
206    let mut result = String::new();
207    let mut chars = input.chars().peekable();
208
209    while let Some(ch) = chars.next() {
210        if ch == '$' {
211            // Check for command substitution $(...) or arithmetic expansion $((...))
212            if let Some(&'(') = chars.peek() {
213                chars.next(); // consume first (
214
215                // Check if this is arithmetic expansion $((...))
216                if let Some(&'(') = chars.peek() {
217                    // Arithmetic expansion $((...))
218                    chars.next(); // consume second (
219                    let mut arithmetic_expr = String::new();
220                    let mut paren_depth = 1;
221                    let mut found_closing = false;
222
223                    while let Some(c) = chars.next() {
224                        if c == '(' {
225                            paren_depth += 1;
226                            arithmetic_expr.push(c);
227                        } else if c == ')' {
228                            paren_depth -= 1;
229                            if paren_depth == 0 {
230                                // Found the first closing ) - check for second )
231                                if let Some(&')') = chars.peek() {
232                                    chars.next(); // consume the second )
233                                    found_closing = true;
234                                    break;
235                                } else {
236                                    // Missing second closing paren, treat as error
237                                    result.push_str("$((");
238                                    result.push_str(&arithmetic_expr);
239                                    result.push(')');
240                                    break;
241                                }
242                            }
243                            arithmetic_expr.push(c);
244                        } else {
245                            arithmetic_expr.push(c);
246                        }
247                    }
248
249                    if found_closing {
250                        // First expand variables in the arithmetic expression
251                        // The arithmetic evaluator expects variable names without $ prefix
252                        // So we need to expand $VAR to the value before evaluation
253                        let mut expanded_expr = String::new();
254                        let mut expr_chars = arithmetic_expr.chars().peekable();
255
256                        while let Some(ch) = expr_chars.next() {
257                            if ch == '$' {
258                                // Expand variable
259                                let mut var_name = String::new();
260                                if let Some(&c) = expr_chars.peek() {
261                                    if c == '?'
262                                        || c == '$'
263                                        || c == '0'
264                                        || c == '#'
265                                        || c == '*'
266                                        || c == '@'
267                                        || c.is_ascii_digit()
268                                    {
269                                        var_name.push(c);
270                                        expr_chars.next();
271                                    } else {
272                                        while let Some(&c) = expr_chars.peek() {
273                                            if c.is_alphanumeric() || c == '_' {
274                                                var_name.push(c);
275                                                expr_chars.next();
276                                            } else {
277                                                break;
278                                            }
279                                        }
280                                    }
281                                }
282
283                                if !var_name.is_empty() {
284                                    if let Some(value) = shell_state.get_var(&var_name) {
285                                        expanded_expr.push_str(&value);
286                                    } else {
287                                        // Variable not found, use 0 for arithmetic
288                                        expanded_expr.push('0');
289                                    }
290                                } else {
291                                    expanded_expr.push('$');
292                                }
293                            } else {
294                                expanded_expr.push(ch);
295                            }
296                        }
297
298                        match crate::arithmetic::evaluate_arithmetic_expression(
299                            &expanded_expr,
300                            shell_state,
301                        ) {
302                            Ok(value) => {
303                                result.push_str(&value.to_string());
304                            }
305                            Err(e) => {
306                                // On arithmetic error, display a proper error message
307                                if shell_state.colors_enabled {
308                                    result.push_str(&format!(
309                                        "{}arithmetic error: {}{}",
310                                        shell_state.color_scheme.error, e, "\x1b[0m"
311                                    ));
312                                } else {
313                                    result.push_str(&format!("arithmetic error: {}", e));
314                                }
315                            }
316                        }
317                    } else {
318                        // Didn't find proper closing - keep as literal
319                        result.push_str("$((");
320                        result.push_str(&arithmetic_expr);
321                        // Note: we don't add closing parens since they weren't in the input
322                    }
323                    continue;
324                }
325
326                // Regular command substitution $(...)
327                let mut sub_command = String::new();
328                let mut paren_depth = 1;
329
330                for c in chars.by_ref() {
331                    if c == '(' {
332                        paren_depth += 1;
333                        sub_command.push(c);
334                    } else if c == ')' {
335                        paren_depth -= 1;
336                        if paren_depth == 0 {
337                            break;
338                        }
339                        sub_command.push(c);
340                    } else {
341                        sub_command.push(c);
342                    }
343                }
344
345                // Execute the command substitution within the current shell context
346                // Parse and execute the command using our own lexer/parser/executor
347                if let Ok(tokens) = crate::lexer::lex(&sub_command, shell_state) {
348                    // Expand aliases before parsing
349                    let expanded_tokens = match crate::lexer::expand_aliases(
350                        tokens,
351                        shell_state,
352                        &mut std::collections::HashSet::new(),
353                    ) {
354                        Ok(t) => t,
355                        Err(_) => {
356                            // Alias expansion error, keep literal
357                            result.push_str("$(");
358                            result.push_str(&sub_command);
359                            result.push(')');
360                            continue;
361                        }
362                    };
363
364                    match crate::parser::parse(expanded_tokens) {
365                        Ok(ast) => {
366                            // Execute within current shell context and capture output
367                            match execute_and_capture_output(ast, shell_state) {
368                                Ok(output) => {
369                                    result.push_str(&output);
370                                }
371                                Err(_) => {
372                                    // On failure, keep the literal
373                                    result.push_str("$(");
374                                    result.push_str(&sub_command);
375                                    result.push(')');
376                                }
377                            }
378                        }
379                        Err(_parse_err) => {
380                            // Parse error - try to handle as function call if it looks like one
381                            let tokens_str = sub_command.trim();
382                            if tokens_str.contains(' ') {
383                                // Split by spaces and check if first token looks like a function call
384                                let parts: Vec<&str> = tokens_str.split_whitespace().collect();
385                                if let Some(first_token) = parts.first()
386                                    && shell_state.get_function(first_token).is_some()
387                                {
388                                    // This is a function call, create AST manually
389                                    let function_call = Ast::FunctionCall {
390                                        name: first_token.to_string(),
391                                        args: parts[1..].iter().map(|s| s.to_string()).collect(),
392                                    };
393                                    match execute_and_capture_output(function_call, shell_state) {
394                                        Ok(output) => {
395                                            result.push_str(&output);
396                                            continue;
397                                        }
398                                        Err(_) => {
399                                            // Fall back to literal
400                                        }
401                                    }
402                                }
403                            }
404                            // Keep the literal
405                            result.push_str("$(");
406                            result.push_str(&sub_command);
407                            result.push(')');
408                        }
409                    }
410                } else {
411                    // Lex error, keep literal
412                    result.push_str("$(");
413                    result.push_str(&sub_command);
414                    result.push(')');
415                }
416            } else {
417                // Regular variable
418                let mut var_name = String::new();
419                let mut next_ch = chars.peek();
420
421                // Handle special single-character variables first
422                if let Some(&c) = next_ch {
423                    if c == '?' || c == '$' || c == '0' || c == '#' || c == '*' || c == '@' {
424                        var_name.push(c);
425                        chars.next(); // consume the character
426                    } else if c.is_ascii_digit() {
427                        // Positional parameter
428                        var_name.push(c);
429                        chars.next();
430                    } else {
431                        // Regular variable name
432                        while let Some(&c) = next_ch {
433                            if c.is_alphanumeric() || c == '_' {
434                                var_name.push(c);
435                                chars.next(); // consume the character
436                                next_ch = chars.peek();
437                            } else {
438                                break;
439                            }
440                        }
441                    }
442                }
443
444                if !var_name.is_empty() {
445                    if let Some(value) = shell_state.get_var(&var_name) {
446                        result.push_str(&value);
447                    } else {
448                        // Variable not found - for positional parameters, expand to empty string
449                        // For other variables, keep the literal
450                        if var_name.chars().next().unwrap().is_ascii_digit()
451                            || var_name == "?"
452                            || var_name == "$"
453                            || var_name == "0"
454                            || var_name == "#"
455                            || var_name == "*"
456                            || var_name == "@"
457                        {
458                            // Expand to empty string for undefined positional parameters
459                        } else {
460                            // Keep the literal for regular variables
461                            result.push('$');
462                            result.push_str(&var_name);
463                        }
464                    }
465                } else {
466                    result.push('$');
467                }
468            }
469        } else if ch == '`' {
470            // Backtick command substitution
471            let mut sub_command = String::new();
472
473            for c in chars.by_ref() {
474                if c == '`' {
475                    break;
476                }
477                sub_command.push(c);
478            }
479
480            // Execute the command substitution
481            if let Ok(tokens) = crate::lexer::lex(&sub_command, shell_state) {
482                // Expand aliases before parsing
483                let expanded_tokens = match crate::lexer::expand_aliases(
484                    tokens,
485                    shell_state,
486                    &mut std::collections::HashSet::new(),
487                ) {
488                    Ok(t) => t,
489                    Err(_) => {
490                        // Alias expansion error, keep literal
491                        result.push('`');
492                        result.push_str(&sub_command);
493                        result.push('`');
494                        continue;
495                    }
496                };
497
498                if let Ok(ast) = crate::parser::parse(expanded_tokens) {
499                    // Execute and capture output
500                    match execute_and_capture_output(ast, shell_state) {
501                        Ok(output) => {
502                            result.push_str(&output);
503                        }
504                        Err(_) => {
505                            // On failure, keep the literal
506                            result.push('`');
507                            result.push_str(&sub_command);
508                            result.push('`');
509                        }
510                    }
511                } else {
512                    // Parse error, keep literal
513                    result.push('`');
514                    result.push_str(&sub_command);
515                    result.push('`');
516                }
517            } else {
518                // Lex error, keep literal
519                result.push('`');
520                result.push_str(&sub_command);
521                result.push('`');
522            }
523        } else {
524            result.push(ch);
525        }
526    }
527
528    result
529}
530
531fn expand_wildcards(args: &[String]) -> Result<Vec<String>, String> {
532    let mut expanded_args = Vec::new();
533
534    for arg in args {
535        if arg.contains('*') || arg.contains('?') || arg.contains('[') {
536            // Try to expand wildcard
537            match glob::glob(arg) {
538                Ok(paths) => {
539                    let mut matches: Vec<String> = paths
540                        .filter_map(|p| p.ok())
541                        .map(|p| p.to_string_lossy().to_string())
542                        .collect();
543                    if matches.is_empty() {
544                        // No matches, keep literal
545                        expanded_args.push(arg.clone());
546                    } else {
547                        // Sort for consistent behavior
548                        matches.sort();
549                        expanded_args.extend(matches);
550                    }
551                }
552                Err(_e) => {
553                    // Invalid pattern, keep literal
554                    expanded_args.push(arg.clone());
555                }
556            }
557        } else {
558            expanded_args.push(arg.clone());
559        }
560    }
561    Ok(expanded_args)
562}
563
564/// Collect here-document content from stdin until the specified delimiter is found
565/// This function reads from stdin line by line until it finds a line that exactly matches the delimiter
566/// If shell_state has pending_heredoc_content, it uses that instead (for script execution)
567fn collect_here_document_content(delimiter: &str, shell_state: &mut ShellState) -> String {
568    // Check if we have pending here-document content from script execution
569    if let Some(content) = shell_state.pending_heredoc_content.take() {
570        return content;
571    }
572
573    // Otherwise, read from stdin (interactive mode)
574    let stdin = std::io::stdin();
575    let mut reader = BufReader::new(stdin.lock());
576    let mut content = String::new();
577    let mut line = String::new();
578
579    loop {
580        line.clear();
581        match reader.read_line(&mut line) {
582            Ok(0) => {
583                // EOF reached
584                break;
585            }
586            Ok(_) => {
587                // Check if this line (without trailing newline) matches the delimiter
588                let line_content = line.trim_end();
589                if line_content == delimiter {
590                    // Found the delimiter, stop collecting
591                    break;
592                } else {
593                    // This is content, add it to our collection
594                    content.push_str(&line);
595                }
596            }
597            Err(e) => {
598                if shell_state.colors_enabled {
599                    eprintln!(
600                        "{}Error reading here-document content: {}\x1b[0m",
601                        shell_state.color_scheme.error, e
602                    );
603                } else {
604                    eprintln!("Error reading here-document content: {}", e);
605                }
606                break;
607            }
608        }
609    }
610
611    content
612}
613
614/// Execute a trap handler command
615/// Note: Signal masking during trap execution will be added in a future update
616pub fn execute_trap_handler(trap_cmd: &str, shell_state: &mut ShellState) -> i32 {
617    // Save current exit code to preserve it across trap execution
618    let saved_exit_code = shell_state.last_exit_code;
619
620    // TODO: Add signal masking to prevent recursive trap calls
621    // This requires careful handling of the nix sigprocmask API
622    // For now, traps execute without signal masking
623
624    // Parse and execute the trap command
625    let result = match crate::lexer::lex(trap_cmd, shell_state) {
626        Ok(tokens) => {
627            match crate::lexer::expand_aliases(
628                tokens,
629                shell_state,
630                &mut std::collections::HashSet::new(),
631            ) {
632                Ok(expanded_tokens) => {
633                    match crate::parser::parse(expanded_tokens) {
634                        Ok(ast) => execute(ast, shell_state),
635                        Err(_) => {
636                            // Parse error in trap handler - silently continue
637                            saved_exit_code
638                        }
639                    }
640                }
641                Err(_) => {
642                    // Alias expansion error - silently continue
643                    saved_exit_code
644                }
645            }
646        }
647        Err(_) => {
648            // Lex error in trap handler - silently continue
649            saved_exit_code
650        }
651    };
652
653    // Restore the original exit code (trap handlers don't affect $?)
654    shell_state.last_exit_code = saved_exit_code;
655
656    result
657}
658
659pub fn execute(ast: Ast, shell_state: &mut ShellState) -> i32 {
660    match ast {
661        Ast::Assignment { var, value } => {
662            // Expand variables and command substitutions in the value
663            let expanded_value = expand_variables_in_string(&value, shell_state);
664            shell_state.set_var(&var, expanded_value);
665            0
666        }
667        Ast::LocalAssignment { var, value } => {
668            // Expand variables and command substitutions in the value
669            let expanded_value = expand_variables_in_string(&value, shell_state);
670            shell_state.set_local_var(&var, expanded_value);
671            0
672        }
673        Ast::Pipeline(commands) => {
674            if commands.is_empty() {
675                return 0;
676            }
677
678            if commands.len() == 1 {
679                // Single command, handle redirections
680                execute_single_command(&commands[0], shell_state)
681            } else {
682                // Pipeline
683                execute_pipeline(&commands, shell_state)
684            }
685        }
686        Ast::Sequence(asts) => {
687            let mut exit_code = 0;
688            for ast in asts {
689                exit_code = execute(ast, shell_state);
690
691                // Check if we got an early return from a function
692                if shell_state.is_returning() {
693                    return exit_code;
694                }
695
696                // Check if exit was requested (e.g., from trap handler)
697                if shell_state.exit_requested {
698                    return shell_state.exit_code;
699                }
700            }
701            exit_code
702        }
703        Ast::If {
704            branches,
705            else_branch,
706        } => {
707            for (condition, then_branch) in branches {
708                let cond_exit = execute(*condition, shell_state);
709                if cond_exit == 0 {
710                    let exit_code = execute(*then_branch, shell_state);
711
712                    // Check if we got an early return from a function
713                    if shell_state.is_returning() {
714                        return exit_code;
715                    }
716
717                    return exit_code;
718                }
719            }
720            if let Some(else_b) = else_branch {
721                let exit_code = execute(*else_b, shell_state);
722
723                // Check if we got an early return from a function
724                if shell_state.is_returning() {
725                    return exit_code;
726                }
727
728                exit_code
729            } else {
730                0
731            }
732        }
733        Ast::Case {
734            word,
735            cases,
736            default,
737        } => {
738            for (patterns, branch) in cases {
739                for pattern in &patterns {
740                    if let Ok(glob_pattern) = glob::Pattern::new(pattern) {
741                        if glob_pattern.matches(&word) {
742                            let exit_code = execute(branch, shell_state);
743
744                            // Check if we got an early return from a function
745                            if shell_state.is_returning() {
746                                return exit_code;
747                            }
748
749                            return exit_code;
750                        }
751                    } else {
752                        // If pattern is invalid, fall back to exact match
753                        if &word == pattern {
754                            let exit_code = execute(branch, shell_state);
755
756                            // Check if we got an early return from a function
757                            if shell_state.is_returning() {
758                                return exit_code;
759                            }
760
761                            return exit_code;
762                        }
763                    }
764                }
765            }
766            if let Some(def) = default {
767                let exit_code = execute(*def, shell_state);
768
769                // Check if we got an early return from a function
770                if shell_state.is_returning() {
771                    return exit_code;
772                }
773
774                exit_code
775            } else {
776                0
777            }
778        }
779        Ast::For {
780            variable,
781            items,
782            body,
783        } => {
784            let mut exit_code = 0;
785
786            // Execute the loop body for each item
787            for item in items {
788                // Process any pending signals before executing the body
789                crate::state::process_pending_signals(shell_state);
790
791                // Check if exit was requested (e.g., from trap handler)
792                if shell_state.exit_requested {
793                    return shell_state.exit_code;
794                }
795
796                // Set the loop variable
797                shell_state.set_var(&variable, item.clone());
798
799                // Execute the body
800                exit_code = execute(*body.clone(), shell_state);
801
802                // Check if we got an early return from a function
803                if shell_state.is_returning() {
804                    return exit_code;
805                }
806
807                // Check if exit was requested after executing the body
808                if shell_state.exit_requested {
809                    return shell_state.exit_code;
810                }
811            }
812
813            exit_code
814        }
815        Ast::While { condition, body } => {
816            let mut exit_code = 0;
817
818            // Execute the loop while condition is true (exit code 0)
819            loop {
820                // Evaluate the condition
821                let cond_exit = execute(*condition.clone(), shell_state);
822
823                // Check if we got an early return from a function
824                if shell_state.is_returning() {
825                    return cond_exit;
826                }
827
828                // Check if exit was requested (e.g., from trap handler)
829                if shell_state.exit_requested {
830                    return shell_state.exit_code;
831                }
832
833                // If condition is false (non-zero exit code), break
834                if cond_exit != 0 {
835                    break;
836                }
837
838                // Execute the body
839                exit_code = execute(*body.clone(), shell_state);
840
841                // Check if we got an early return from a function
842                if shell_state.is_returning() {
843                    return exit_code;
844                }
845
846                // Check if exit was requested (e.g., from trap handler)
847                if shell_state.exit_requested {
848                    return shell_state.exit_code;
849                }
850            }
851
852            exit_code
853        }
854        Ast::FunctionDefinition { name, body } => {
855            // Store function definition in shell state
856            shell_state.define_function(name.clone(), *body);
857            0
858        }
859        Ast::FunctionCall { name, args } => {
860            if let Some(function_body) = shell_state.get_function(&name).cloned() {
861                // Check recursion limit before entering function
862                if shell_state.function_depth >= shell_state.max_recursion_depth {
863                    eprintln!(
864                        "Function recursion limit ({}) exceeded",
865                        shell_state.max_recursion_depth
866                    );
867                    return 1;
868                }
869
870                // Enter function context for local variable scoping
871                shell_state.enter_function();
872
873                // Set up arguments as regular variables (will be enhanced in Phase 2)
874                let old_positional = shell_state.positional_params.clone();
875
876                // Set positional parameters for function arguments
877                shell_state.set_positional_params(args.clone());
878
879                // Execute function body
880                let exit_code = execute(function_body, shell_state);
881
882                // Check if we got an early return from the function
883                if shell_state.is_returning() {
884                    let return_value = shell_state.get_return_value().unwrap_or(0);
885
886                    // Restore old positional parameters
887                    shell_state.set_positional_params(old_positional);
888
889                    // Exit function context
890                    shell_state.exit_function();
891
892                    // Clear return state
893                    shell_state.clear_return();
894
895                    // Return the early return value
896                    return return_value;
897                }
898
899                // Restore old positional parameters
900                shell_state.set_positional_params(old_positional);
901
902                // Exit function context
903                shell_state.exit_function();
904
905                exit_code
906            } else {
907                eprintln!("Function '{}' not found", name);
908                1
909            }
910        }
911        Ast::Return { value } => {
912            // Return statements can only be used inside functions
913            if shell_state.function_depth == 0 {
914                eprintln!("Return statement outside of function");
915                return 1;
916            }
917
918            // Parse return value if provided
919            let exit_code = if let Some(ref val) = value {
920                val.parse::<i32>().unwrap_or(0)
921            } else {
922                0
923            };
924
925            // Set return state to indicate early return from function
926            shell_state.set_return(exit_code);
927
928            // Return the exit code - the function call handler will check for this
929            exit_code
930        }
931        Ast::And { left, right } => {
932            // Execute left side first
933            let left_exit = execute(*left, shell_state);
934
935            // Check if we got an early return from a function
936            if shell_state.is_returning() {
937                return left_exit;
938            }
939
940            // Only execute right side if left succeeded (exit code 0)
941            if left_exit == 0 {
942                execute(*right, shell_state)
943            } else {
944                left_exit
945            }
946        }
947        Ast::Or { left, right } => {
948            // Execute left side first
949            let left_exit = execute(*left, shell_state);
950
951            // Check if we got an early return from a function
952            if shell_state.is_returning() {
953                return left_exit;
954            }
955
956            // Only execute right side if left failed (exit code != 0)
957            if left_exit != 0 {
958                execute(*right, shell_state)
959            } else {
960                left_exit
961            }
962        }
963    }
964}
965
966fn execute_single_command(cmd: &ShellCommand, shell_state: &mut ShellState) -> i32 {
967    if cmd.args.is_empty() {
968        return 0;
969    }
970
971    // First expand variables, then wildcards
972    let var_expanded_args = expand_variables_in_args(&cmd.args, shell_state);
973    let expanded_args = match expand_wildcards(&var_expanded_args) {
974        Ok(args) => args,
975        Err(_) => return 1,
976    };
977
978    if expanded_args.is_empty() {
979        return 0;
980    }
981
982    // Check if this is a function call
983    if shell_state.get_function(&expanded_args[0]).is_some() {
984        // This is a function call - create a FunctionCall AST node and execute it
985        let function_call = Ast::FunctionCall {
986            name: expanded_args[0].clone(),
987            args: expanded_args[1..].to_vec(),
988        };
989        return execute(function_call, shell_state);
990    }
991
992    if crate::builtins::is_builtin(&expanded_args[0]) {
993        // Create a temporary ShellCommand with expanded args
994        let temp_cmd = ShellCommand {
995            args: expanded_args,
996            input: cmd.input.clone(),
997            output: cmd.output.clone(),
998            append: cmd.append.clone(),
999            here_doc_delimiter: None,
1000            here_doc_quoted: false,
1001            here_string_content: None,
1002        };
1003
1004        // If we're capturing output, create a writer for it
1005        if let Some(ref capture_buffer) = shell_state.capture_output.clone() {
1006            // Create a writer that writes to our capture buffer
1007            struct CaptureWriter {
1008                buffer: Rc<RefCell<Vec<u8>>>,
1009            }
1010            impl std::io::Write for CaptureWriter {
1011                fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1012                    self.buffer.borrow_mut().extend_from_slice(buf);
1013                    Ok(buf.len())
1014                }
1015                fn flush(&mut self) -> std::io::Result<()> {
1016                    Ok(())
1017                }
1018            }
1019            let writer = CaptureWriter {
1020                buffer: capture_buffer.clone(),
1021            };
1022            crate::builtins::execute_builtin(&temp_cmd, shell_state, Some(Box::new(writer)))
1023        } else {
1024            crate::builtins::execute_builtin(&temp_cmd, shell_state, None)
1025        }
1026    } else {
1027        // Separate environment variable assignments from the actual command
1028        // Environment vars must come before the command and have the form VAR=value
1029        let mut env_assignments = Vec::new();
1030        let mut command_start_idx = 0;
1031
1032        for (idx, arg) in expanded_args.iter().enumerate() {
1033            // Check if this looks like an environment variable assignment
1034            if let Some(eq_pos) = arg.find('=')
1035                && eq_pos > 0
1036            {
1037                let var_part = &arg[..eq_pos];
1038                // Check if var_part is a valid variable name
1039                if var_part
1040                    .chars()
1041                    .next()
1042                    .map(|c| c.is_alphabetic() || c == '_')
1043                    .unwrap_or(false)
1044                    && var_part.chars().all(|c| c.is_alphanumeric() || c == '_')
1045                {
1046                    env_assignments.push(arg.clone());
1047                    command_start_idx = idx + 1;
1048                    continue;
1049                }
1050            }
1051            // If we reach here, this is not an env assignment, so we've found the command
1052            break;
1053        }
1054
1055        // Check if we have a command to execute (vs just env assignments)
1056        let has_command = command_start_idx < expanded_args.len();
1057
1058        // If all args were env assignments, set them in the shell
1059        // but continue to process redirections per POSIX
1060        if !has_command {
1061            for assignment in &env_assignments {
1062                if let Some(eq_pos) = assignment.find('=') {
1063                    let var_name = &assignment[..eq_pos];
1064                    let var_value = &assignment[eq_pos + 1..];
1065                    shell_state.set_var(var_name, var_value.to_string());
1066                }
1067            }
1068        }
1069
1070        // Prepare command if we have one
1071        let mut command = if has_command {
1072            let mut cmd = Command::new(&expanded_args[command_start_idx]);
1073            cmd.args(&expanded_args[command_start_idx + 1..]);
1074
1075            // Set environment for child process
1076            let mut child_env = shell_state.get_env_for_child();
1077
1078            // Add the per-command environment variable assignments
1079            for assignment in env_assignments {
1080                if let Some(eq_pos) = assignment.find('=') {
1081                    let var_name = assignment[..eq_pos].to_string();
1082                    let var_value = assignment[eq_pos + 1..].to_string();
1083                    child_env.insert(var_name, var_value);
1084                }
1085            }
1086
1087            cmd.env_clear();
1088            for (key, value) in child_env {
1089                cmd.env(key, value);
1090            }
1091
1092            // If we're capturing output, redirect stdout to capture buffer
1093            let capturing = shell_state.capture_output.is_some();
1094            if capturing {
1095                cmd.stdout(Stdio::piped());
1096            }
1097
1098            Some(cmd)
1099        } else {
1100            None
1101        };
1102
1103        // Handle input redirection (process even if no command)
1104        if let Some(ref input_file) = cmd.input {
1105            let expanded_input = expand_variables_in_string(input_file, shell_state);
1106            if let Some(ref mut command) = command {
1107                match File::open(&expanded_input) {
1108                    Ok(file) => {
1109                        command.stdin(Stdio::from(file));
1110                    }
1111                    Err(e) => {
1112                        if shell_state.colors_enabled {
1113                            eprintln!(
1114                                "{}Error opening input file '{}{}",
1115                                shell_state.color_scheme.error,
1116                                input_file,
1117                                &format!("': {}\x1b[0m", e)
1118                            );
1119                        } else {
1120                            eprintln!("Error opening input file '{}': {}", input_file, e);
1121                        }
1122                        return 1;
1123                    }
1124                }
1125            } else {
1126                // No command but redirection - just verify file exists for side effects
1127                match File::open(&expanded_input) {
1128                    Ok(_) => {
1129                        // File opened successfully, side effect complete
1130                    }
1131                    Err(e) => {
1132                        if shell_state.colors_enabled {
1133                            eprintln!(
1134                                "{}Error opening input file '{}{}",
1135                                shell_state.color_scheme.error,
1136                                input_file,
1137                                &format!("': {}\x1b[0m", e)
1138                            );
1139                        } else {
1140                            eprintln!("Error opening input file '{}': {}", input_file, e);
1141                        }
1142                        return 1;
1143                    }
1144                }
1145            }
1146        } else if let Some(ref delimiter) = cmd.here_doc_delimiter {
1147            // Handle here-document redirection (process even if no command)
1148            let here_doc_content = collect_here_document_content(delimiter, shell_state);
1149            // Expand variables and command substitutions ONLY if delimiter was not quoted
1150            // Quoted delimiters (<<'EOF' or <<"EOF") disable expansion per POSIX
1151            let expanded_content = if cmd.here_doc_quoted {
1152                here_doc_content.clone() // No expansion for quoted delimiters
1153            } else {
1154                expand_variables_in_string(&here_doc_content, shell_state)
1155            };
1156
1157            if let Some(ref mut command) = command {
1158                let pipe_result = pipe();
1159                match pipe_result {
1160                    Ok((reader, mut writer)) => {
1161                        use std::io::Write;
1162                        if let Err(e) = writeln!(writer, "{}", expanded_content) {
1163                            if shell_state.colors_enabled {
1164                                eprintln!(
1165                                    "{}Error writing here-document content: {}\x1b[0m",
1166                                    shell_state.color_scheme.error, e
1167                                );
1168                            } else {
1169                                eprintln!("Error writing here-document content: {}", e);
1170                            }
1171                            return 1;
1172                        }
1173                        // Note: writer will be closed when it goes out of scope
1174                        command.stdin(Stdio::from(reader));
1175                    }
1176                    Err(e) => {
1177                        if shell_state.colors_enabled {
1178                            eprintln!(
1179                                "{}Error creating pipe for here-document: {}\x1b[0m",
1180                                shell_state.color_scheme.error, e
1181                            );
1182                        } else {
1183                            eprintln!("Error creating pipe for here-document: {}", e);
1184                        }
1185                        return 1;
1186                    }
1187                }
1188            }
1189            // If no command, here-doc content was consumed for side effects (POSIX requirement)
1190        } else if let Some(ref content) = cmd.here_string_content {
1191            // Handle here-string redirection (process even if no command)
1192            let expanded_content = expand_variables_in_string(content, shell_state);
1193
1194            if let Some(ref mut command) = command {
1195                let pipe_result = pipe();
1196                match pipe_result {
1197                    Ok((reader, mut writer)) => {
1198                        use std::io::Write;
1199                        if let Err(e) = write!(writer, "{}", expanded_content) {
1200                            if shell_state.colors_enabled {
1201                                eprintln!(
1202                                    "{}Error writing here-string content: {}\x1b[0m",
1203                                    shell_state.color_scheme.error, e
1204                                );
1205                            } else {
1206                                eprintln!("Error writing here-string content: {}", e);
1207                            }
1208                            return 1;
1209                        }
1210                        // Note: writer will be closed when it goes out of scope
1211                        command.stdin(Stdio::from(reader));
1212                    }
1213                    Err(e) => {
1214                        if shell_state.colors_enabled {
1215                            eprintln!(
1216                                "{}Error creating pipe for here-string: {}\x1b[0m",
1217                                shell_state.color_scheme.error, e
1218                            );
1219                        } else {
1220                            eprintln!("Error creating pipe for here-string: {}", e);
1221                        }
1222                        return 1;
1223                    }
1224                }
1225            }
1226            // If no command, here-string was processed for side effects
1227        }
1228
1229        // Handle output redirection (process even if no command)
1230        if let Some(ref output_file) = cmd.output {
1231            let expanded_output = expand_variables_in_string(output_file, shell_state);
1232            match File::create(&expanded_output) {
1233                Ok(file) => {
1234                    if let Some(ref mut command) = command {
1235                        command.stdout(Stdio::from(file));
1236                    }
1237                    // If no command, file was created for side effects (POSIX requirement)
1238                }
1239                Err(e) => {
1240                    if shell_state.colors_enabled {
1241                        eprintln!(
1242                            "{}Error creating output file '{}{}",
1243                            shell_state.color_scheme.error,
1244                            output_file,
1245                            &format!("': {}\x1b[0m", e)
1246                        );
1247                    } else {
1248                        eprintln!("Error creating output file '{}': {}", output_file, e);
1249                    }
1250                    return 1;
1251                }
1252            }
1253        } else if let Some(ref append_file) = cmd.append {
1254            let expanded_append = expand_variables_in_string(append_file, shell_state);
1255            match File::options()
1256                .append(true)
1257                .create(true)
1258                .open(&expanded_append)
1259            {
1260                Ok(file) => {
1261                    if let Some(ref mut command) = command {
1262                        command.stdout(Stdio::from(file));
1263                    }
1264                    // If no command, file was opened/created for side effects (POSIX requirement)
1265                }
1266                Err(e) => {
1267                    if shell_state.colors_enabled {
1268                        eprintln!(
1269                            "{}Error opening append file '{}{}",
1270                            shell_state.color_scheme.error,
1271                            append_file,
1272                            &format!("': {}\x1b[0m", e)
1273                        );
1274                    } else {
1275                        eprintln!("Error opening append file '{}': {}", append_file, e);
1276                    }
1277                    return 1;
1278                }
1279            }
1280        }
1281
1282        // If no command to execute, return success after processing redirections
1283        let Some(mut command) = command else {
1284            return 0;
1285        };
1286
1287        // Check if we're capturing output for this command
1288        let capturing = shell_state.capture_output.is_some();
1289
1290        match command.spawn() {
1291            Ok(mut child) => {
1292                // If capturing, read stdout
1293                if capturing && let Some(mut stdout) = child.stdout.take() {
1294                    use std::io::Read;
1295                    let mut output = Vec::new();
1296                    if stdout.read_to_end(&mut output).is_ok()
1297                        && let Some(ref capture_buffer) = shell_state.capture_output
1298                    {
1299                        capture_buffer.borrow_mut().extend_from_slice(&output);
1300                    }
1301                }
1302
1303                match child.wait() {
1304                    Ok(status) => status.code().unwrap_or(0),
1305                    Err(e) => {
1306                        if shell_state.colors_enabled {
1307                            eprintln!(
1308                                "{}Error waiting for command: {}\x1b[0m",
1309                                shell_state.color_scheme.error, e
1310                            );
1311                        } else {
1312                            eprintln!("Error waiting for command: {}", e);
1313                        }
1314                        1
1315                    }
1316                }
1317            }
1318            Err(e) => {
1319                if shell_state.colors_enabled {
1320                    eprintln!(
1321                        "{}Command spawn error: {}\x1b[0m",
1322                        shell_state.color_scheme.error, e
1323                    );
1324                } else {
1325                    eprintln!("Command spawn error: {}", e);
1326                }
1327                1
1328            }
1329        }
1330    }
1331}
1332
1333fn execute_pipeline(commands: &[ShellCommand], shell_state: &mut ShellState) -> i32 {
1334    let mut exit_code = 0;
1335    let mut previous_stdout = None;
1336
1337    for (i, cmd) in commands.iter().enumerate() {
1338        if cmd.args.is_empty() {
1339            continue;
1340        }
1341
1342        let is_last = i == commands.len() - 1;
1343
1344        // First expand variables, then wildcards
1345        let var_expanded_args = expand_variables_in_args(&cmd.args, shell_state);
1346        let expanded_args = match expand_wildcards(&var_expanded_args) {
1347            Ok(args) => args,
1348            Err(_) => return 1,
1349        };
1350
1351        if expanded_args.is_empty() {
1352            continue;
1353        }
1354
1355        if crate::builtins::is_builtin(&expanded_args[0]) {
1356            // Built-ins in pipelines are tricky - for now, execute them separately
1357            // This is not perfect but better than nothing
1358            let temp_cmd = ShellCommand {
1359                args: expanded_args,
1360                input: cmd.input.clone(),
1361                output: cmd.output.clone(),
1362                append: cmd.append.clone(),
1363                here_doc_delimiter: None,
1364                here_doc_quoted: false,
1365                here_string_content: None,
1366            };
1367            if !is_last {
1368                // Create a safe pipe
1369                let (reader, writer) = match pipe() {
1370                    Ok(p) => p,
1371                    Err(e) => {
1372                        if shell_state.colors_enabled {
1373                            eprintln!(
1374                                "{}Error creating pipe for builtin: {}\x1b[0m",
1375                                shell_state.color_scheme.error, e
1376                            );
1377                        } else {
1378                            eprintln!("Error creating pipe for builtin: {}", e);
1379                        }
1380                        return 1;
1381                    }
1382                };
1383                // Execute builtin with writer for output capture
1384                exit_code = crate::builtins::execute_builtin(
1385                    &temp_cmd,
1386                    shell_state,
1387                    Some(Box::new(writer)),
1388                );
1389                // Use reader for next command's stdin
1390                previous_stdout = Some(Stdio::from(reader));
1391            } else {
1392                // Last command: check if we're capturing output
1393                if let Some(ref capture_buffer) = shell_state.capture_output.clone() {
1394                    // Create a writer that writes to our capture buffer
1395                    struct CaptureWriter {
1396                        buffer: Rc<RefCell<Vec<u8>>>,
1397                    }
1398                    impl std::io::Write for CaptureWriter {
1399                        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1400                            self.buffer.borrow_mut().extend_from_slice(buf);
1401                            Ok(buf.len())
1402                        }
1403                        fn flush(&mut self) -> std::io::Result<()> {
1404                            Ok(())
1405                        }
1406                    }
1407                    let writer = CaptureWriter {
1408                        buffer: capture_buffer.clone(),
1409                    };
1410                    exit_code = crate::builtins::execute_builtin(
1411                        &temp_cmd,
1412                        shell_state,
1413                        Some(Box::new(writer)),
1414                    );
1415                } else {
1416                    // Not capturing, execute normally
1417                    exit_code = crate::builtins::execute_builtin(&temp_cmd, shell_state, None);
1418                }
1419                previous_stdout = None;
1420            }
1421        } else {
1422            let mut command = Command::new(&expanded_args[0]);
1423            command.args(&expanded_args[1..]);
1424
1425            // Set environment for child process
1426            let child_env = shell_state.get_env_for_child();
1427            command.env_clear();
1428            for (key, value) in child_env {
1429                command.env(key, value);
1430            }
1431
1432            // Set stdin from previous command's stdout
1433            if let Some(prev) = previous_stdout.take() {
1434                command.stdin(prev);
1435            }
1436
1437            // Set stdout for next command, or for capturing if this is the last
1438            if !is_last {
1439                command.stdout(Stdio::piped());
1440            } else if shell_state.capture_output.is_some() {
1441                // Last command in pipeline but we're capturing output
1442                command.stdout(Stdio::piped());
1443            }
1444
1445            // Handle input redirection (only for first command)
1446            if i == 0 {
1447                if let Some(ref input_file) = cmd.input {
1448                    let expanded_input = expand_variables_in_string(input_file, shell_state);
1449                    match File::open(&expanded_input) {
1450                        Ok(file) => {
1451                            command.stdin(Stdio::from(file));
1452                        }
1453                        Err(e) => {
1454                            if shell_state.colors_enabled {
1455                                eprintln!(
1456                                    "{}Error opening input file '{}{}",
1457                                    shell_state.color_scheme.error,
1458                                    input_file,
1459                                    &format!("': {}\x1b[0m", e)
1460                                );
1461                            } else {
1462                                eprintln!("Error opening input file '{}': {}", input_file, e);
1463                            }
1464                            return 1;
1465                        }
1466                    }
1467                } else if let Some(ref delimiter) = cmd.here_doc_delimiter {
1468                    // Handle here-document redirection for first command in pipeline
1469                    let here_doc_content = collect_here_document_content(delimiter, shell_state);
1470                    // Expand variables and command substitutions ONLY if delimiter was not quoted
1471                    // Quoted delimiters (<<'EOF' or <<"EOF") disable expansion per POSIX
1472                    let expanded_content = if cmd.here_doc_quoted {
1473                        here_doc_content // No expansion for quoted delimiters
1474                    } else {
1475                        expand_variables_in_string(&here_doc_content, shell_state)
1476                    };
1477                    let pipe_result = pipe();
1478                    match pipe_result {
1479                        Ok((reader, mut writer)) => {
1480                            use std::io::Write;
1481                            if let Err(e) = writeln!(writer, "{}", expanded_content) {
1482                                if shell_state.colors_enabled {
1483                                    eprintln!(
1484                                        "{}Error writing here-document content: {}\x1b[0m",
1485                                        shell_state.color_scheme.error, e
1486                                    );
1487                                } else {
1488                                    eprintln!("Error writing here-document content: {}", e);
1489                                }
1490                                return 1;
1491                            }
1492                            command.stdin(Stdio::from(reader));
1493                        }
1494                        Err(e) => {
1495                            if shell_state.colors_enabled {
1496                                eprintln!(
1497                                    "{}Error creating pipe for here-document: {}\x1b[0m",
1498                                    shell_state.color_scheme.error, e
1499                                );
1500                            } else {
1501                                eprintln!("Error creating pipe for here-document: {}", e);
1502                            }
1503                            return 1;
1504                        }
1505                    }
1506                } else if let Some(ref content) = cmd.here_string_content {
1507                    // Handle here-string redirection for first command in pipeline
1508                    let expanded_content = expand_variables_in_string(content, shell_state);
1509                    let pipe_result = pipe();
1510                    match pipe_result {
1511                        Ok((reader, mut writer)) => {
1512                            use std::io::Write;
1513                            if let Err(e) = write!(writer, "{}", expanded_content) {
1514                                if shell_state.colors_enabled {
1515                                    eprintln!(
1516                                        "{}Error writing here-string content: {}\x1b[0m",
1517                                        shell_state.color_scheme.error, e
1518                                    );
1519                                } else {
1520                                    eprintln!("Error writing here-string content: {}", e);
1521                                }
1522                                return 1;
1523                            }
1524                            command.stdin(Stdio::from(reader));
1525                        }
1526                        Err(e) => {
1527                            if shell_state.colors_enabled {
1528                                eprintln!(
1529                                    "{}Error creating pipe for here-string: {}\x1b[0m",
1530                                    shell_state.color_scheme.error, e
1531                                );
1532                            } else {
1533                                eprintln!("Error creating pipe for here-string: {}", e);
1534                            }
1535                            return 1;
1536                        }
1537                    }
1538                }
1539            }
1540
1541            // Handle output redirection (only for last command)
1542            if is_last {
1543                if let Some(ref output_file) = cmd.output {
1544                    let expanded_output = expand_variables_in_string(output_file, shell_state);
1545                    match File::create(&expanded_output) {
1546                        Ok(file) => {
1547                            command.stdout(Stdio::from(file));
1548                        }
1549                        Err(e) => {
1550                            if shell_state.colors_enabled {
1551                                eprintln!(
1552                                    "{}Error creating output file '{}{}",
1553                                    shell_state.color_scheme.error,
1554                                    output_file,
1555                                    &format!("': {}\x1b[0m", e)
1556                                );
1557                            } else {
1558                                eprintln!("Error creating output file '{}': {}", output_file, e);
1559                            }
1560                            return 1;
1561                        }
1562                    }
1563                } else if let Some(ref append_file) = cmd.append {
1564                    let expanded_append = expand_variables_in_string(append_file, shell_state);
1565                    match File::options()
1566                        .append(true)
1567                        .create(true)
1568                        .open(&expanded_append)
1569                    {
1570                        Ok(file) => {
1571                            command.stdout(Stdio::from(file));
1572                        }
1573                        Err(e) => {
1574                            if shell_state.colors_enabled {
1575                                eprintln!(
1576                                    "{}Error opening append file '{}{}",
1577                                    shell_state.color_scheme.error,
1578                                    append_file,
1579                                    &format!("': {}\x1b[0m", e)
1580                                );
1581                            } else {
1582                                eprintln!("Error opening append file '{}': {}", append_file, e);
1583                            }
1584                            return 1;
1585                        }
1586                    }
1587                }
1588            }
1589
1590            match command.spawn() {
1591                Ok(mut child) => {
1592                    if !is_last {
1593                        previous_stdout = child.stdout.take().map(Stdio::from);
1594                    } else if shell_state.capture_output.is_some() {
1595                        // Last command and we're capturing - read its output
1596                        if let Some(mut stdout) = child.stdout.take() {
1597                            use std::io::Read;
1598                            let mut output = Vec::new();
1599                            if stdout.read_to_end(&mut output).is_ok()
1600                                && let Some(ref capture_buffer) = shell_state.capture_output
1601                            {
1602                                capture_buffer.borrow_mut().extend_from_slice(&output);
1603                            }
1604                        }
1605                    }
1606                    match child.wait() {
1607                        Ok(status) => {
1608                            exit_code = status.code().unwrap_or(0);
1609                        }
1610                        Err(e) => {
1611                            if shell_state.colors_enabled {
1612                                eprintln!(
1613                                    "{}Error waiting for command: {}\x1b[0m",
1614                                    shell_state.color_scheme.error, e
1615                                );
1616                            } else {
1617                                eprintln!("Error waiting for command: {}", e);
1618                            }
1619                            exit_code = 1;
1620                        }
1621                    }
1622                }
1623                Err(e) => {
1624                    if shell_state.colors_enabled {
1625                        eprintln!(
1626                            "{}Error spawning command '{}{}",
1627                            shell_state.color_scheme.error,
1628                            expanded_args[0],
1629                            &format!("': {}\x1b[0m", e)
1630                        );
1631                    } else {
1632                        eprintln!("Error spawning command '{}': {}", expanded_args[0], e);
1633                    }
1634                    exit_code = 1;
1635                }
1636            }
1637        }
1638    }
1639
1640    exit_code
1641}
1642
1643#[cfg(test)]
1644mod tests {
1645    use super::*;
1646
1647    #[test]
1648    fn test_execute_single_command_builtin() {
1649        let cmd = ShellCommand {
1650            args: vec!["true".to_string()],
1651            input: None,
1652            output: None,
1653            append: None,
1654            here_doc_delimiter: None,
1655            here_doc_quoted: false,
1656            here_string_content: None,
1657        };
1658        let mut shell_state = ShellState::new();
1659        let exit_code = execute_single_command(&cmd, &mut shell_state);
1660        assert_eq!(exit_code, 0);
1661    }
1662
1663    // For external commands, test with a command that exists
1664    #[test]
1665    fn test_execute_single_command_external() {
1666        let cmd = ShellCommand {
1667            args: vec!["true".to_string()], // Assume true exists
1668            input: None,
1669            output: None,
1670            append: None,
1671            here_doc_delimiter: None,
1672            here_doc_quoted: false,
1673            here_string_content: None,
1674        };
1675        let mut shell_state = ShellState::new();
1676        let exit_code = execute_single_command(&cmd, &mut shell_state);
1677        assert_eq!(exit_code, 0);
1678    }
1679
1680    #[test]
1681    fn test_execute_single_command_external_nonexistent() {
1682        let cmd = ShellCommand {
1683            args: vec!["nonexistent_command".to_string()],
1684            input: None,
1685            output: None,
1686            append: None,
1687            here_doc_delimiter: None,
1688            here_doc_quoted: false,
1689            here_string_content: None,
1690        };
1691        let mut shell_state = ShellState::new();
1692        let exit_code = execute_single_command(&cmd, &mut shell_state);
1693        assert_eq!(exit_code, 1); // Command not found
1694    }
1695
1696    #[test]
1697    fn test_execute_pipeline() {
1698        let commands = vec![
1699            ShellCommand {
1700                args: vec!["printf".to_string(), "hello".to_string()],
1701                input: None,
1702                output: None,
1703                append: None,
1704                here_doc_delimiter: None,
1705                here_doc_quoted: false,
1706                here_string_content: None,
1707            },
1708            ShellCommand {
1709                args: vec!["cat".to_string()], // cat reads from stdin
1710                input: None,
1711                output: None,
1712                append: None,
1713                here_doc_delimiter: None,
1714                here_doc_quoted: false,
1715                here_string_content: None,
1716            },
1717        ];
1718        let mut shell_state = ShellState::new();
1719        let exit_code = execute_pipeline(&commands, &mut shell_state);
1720        assert_eq!(exit_code, 0);
1721    }
1722
1723    #[test]
1724    fn test_execute_empty_pipeline() {
1725        let commands = vec![];
1726        let mut shell_state = ShellState::new();
1727        let exit_code = execute(Ast::Pipeline(commands), &mut shell_state);
1728        assert_eq!(exit_code, 0);
1729    }
1730
1731    #[test]
1732    fn test_execute_single_command() {
1733        let ast = Ast::Pipeline(vec![ShellCommand {
1734            args: vec!["true".to_string()],
1735            input: None,
1736            output: None,
1737            append: None,
1738            here_doc_delimiter: None,
1739            here_doc_quoted: false,
1740            here_string_content: None,
1741        }]);
1742        let mut shell_state = ShellState::new();
1743        let exit_code = execute(ast, &mut shell_state);
1744        assert_eq!(exit_code, 0);
1745    }
1746
1747    #[test]
1748    fn test_execute_function_definition() {
1749        let ast = Ast::FunctionDefinition {
1750            name: "test_func".to_string(),
1751            body: Box::new(Ast::Pipeline(vec![ShellCommand {
1752                args: vec!["echo".to_string(), "hello".to_string()],
1753                input: None,
1754                output: None,
1755                append: None,
1756                here_doc_delimiter: None,
1757                here_doc_quoted: false,
1758                here_string_content: None,
1759            }])),
1760        };
1761        let mut shell_state = ShellState::new();
1762        let exit_code = execute(ast, &mut shell_state);
1763        assert_eq!(exit_code, 0);
1764
1765        // Check that function was stored
1766        assert!(shell_state.get_function("test_func").is_some());
1767    }
1768
1769    #[test]
1770    fn test_execute_function_call() {
1771        // First define a function
1772        let mut shell_state = ShellState::new();
1773        shell_state.define_function(
1774            "test_func".to_string(),
1775            Ast::Pipeline(vec![ShellCommand {
1776                args: vec!["echo".to_string(), "hello".to_string()],
1777                input: None,
1778                output: None,
1779                append: None,
1780                here_doc_delimiter: None,
1781                here_doc_quoted: false,
1782                here_string_content: None,
1783            }]),
1784        );
1785
1786        // Now call the function
1787        let ast = Ast::FunctionCall {
1788            name: "test_func".to_string(),
1789            args: vec![],
1790        };
1791        let exit_code = execute(ast, &mut shell_state);
1792        assert_eq!(exit_code, 0);
1793    }
1794
1795    #[test]
1796    fn test_execute_function_call_with_args() {
1797        // First define a function that uses arguments
1798        let mut shell_state = ShellState::new();
1799        shell_state.define_function(
1800            "test_func".to_string(),
1801            Ast::Pipeline(vec![ShellCommand {
1802                args: vec!["echo".to_string(), "arg1".to_string()],
1803                input: None,
1804                output: None,
1805                append: None,
1806                here_doc_delimiter: None,
1807                here_doc_quoted: false,
1808                here_string_content: None,
1809            }]),
1810        );
1811
1812        // Now call the function with arguments
1813        let ast = Ast::FunctionCall {
1814            name: "test_func".to_string(),
1815            args: vec!["hello".to_string()],
1816        };
1817        let exit_code = execute(ast, &mut shell_state);
1818        assert_eq!(exit_code, 0);
1819    }
1820
1821    #[test]
1822    fn test_execute_nonexistent_function() {
1823        let mut shell_state = ShellState::new();
1824        let ast = Ast::FunctionCall {
1825            name: "nonexistent".to_string(),
1826            args: vec![],
1827        };
1828        let exit_code = execute(ast, &mut shell_state);
1829        assert_eq!(exit_code, 1); // Should return error code
1830    }
1831
1832    #[test]
1833    fn test_execute_function_integration() {
1834        // Test full integration: define function, then call it
1835        let mut shell_state = ShellState::new();
1836
1837        // First define a function
1838        let define_ast = Ast::FunctionDefinition {
1839            name: "hello".to_string(),
1840            body: Box::new(Ast::Pipeline(vec![ShellCommand {
1841                args: vec!["printf".to_string(), "Hello from function".to_string()],
1842                input: None,
1843                output: None,
1844                append: None,
1845                here_doc_delimiter: None,
1846                here_doc_quoted: false,
1847                here_string_content: None,
1848            }])),
1849        };
1850        let exit_code = execute(define_ast, &mut shell_state);
1851        assert_eq!(exit_code, 0);
1852
1853        // Now call the function
1854        let call_ast = Ast::FunctionCall {
1855            name: "hello".to_string(),
1856            args: vec![],
1857        };
1858        let exit_code = execute(call_ast, &mut shell_state);
1859        assert_eq!(exit_code, 0);
1860    }
1861
1862    #[test]
1863    fn test_execute_function_with_local_variables() {
1864        let mut shell_state = ShellState::new();
1865
1866        // Set a global variable
1867        shell_state.set_var("global_var", "global_value".to_string());
1868
1869        // Define a function that uses local variables
1870        let define_ast = Ast::FunctionDefinition {
1871            name: "test_func".to_string(),
1872            body: Box::new(Ast::Sequence(vec![
1873                Ast::LocalAssignment {
1874                    var: "local_var".to_string(),
1875                    value: "local_value".to_string(),
1876                },
1877                Ast::Assignment {
1878                    var: "global_var".to_string(),
1879                    value: "modified_in_function".to_string(),
1880                },
1881                Ast::Pipeline(vec![ShellCommand {
1882                    args: vec!["printf".to_string(), "success".to_string()],
1883                    input: None,
1884                    output: None,
1885                    append: None,
1886                    here_doc_delimiter: None,
1887                    here_doc_quoted: false,
1888                    here_string_content: None,
1889                }]),
1890            ])),
1891        };
1892        let exit_code = execute(define_ast, &mut shell_state);
1893        assert_eq!(exit_code, 0);
1894
1895        // Global variable should not be modified during function definition
1896        assert_eq!(
1897            shell_state.get_var("global_var"),
1898            Some("global_value".to_string())
1899        );
1900
1901        // Call the function
1902        let call_ast = Ast::FunctionCall {
1903            name: "test_func".to_string(),
1904            args: vec![],
1905        };
1906        let exit_code = execute(call_ast, &mut shell_state);
1907        assert_eq!(exit_code, 0);
1908
1909        // After function call, global variable should be modified since function assignments affect global scope
1910        assert_eq!(
1911            shell_state.get_var("global_var"),
1912            Some("modified_in_function".to_string())
1913        );
1914    }
1915
1916    #[test]
1917    fn test_execute_nested_function_calls() {
1918        let mut shell_state = ShellState::new();
1919
1920        // Set global variable
1921        shell_state.set_var("global_var", "global".to_string());
1922
1923        // Define outer function
1924        let outer_func = Ast::FunctionDefinition {
1925            name: "outer".to_string(),
1926            body: Box::new(Ast::Sequence(vec![
1927                Ast::Assignment {
1928                    var: "global_var".to_string(),
1929                    value: "outer_modified".to_string(),
1930                },
1931                Ast::FunctionCall {
1932                    name: "inner".to_string(),
1933                    args: vec![],
1934                },
1935                Ast::Pipeline(vec![ShellCommand {
1936                    args: vec!["printf".to_string(), "outer_done".to_string()],
1937                    input: None,
1938                    output: None,
1939                    append: None,
1940                    here_doc_delimiter: None,
1941                    here_doc_quoted: false,
1942                    here_string_content: None,
1943                }]),
1944            ])),
1945        };
1946
1947        // Define inner function
1948        let inner_func = Ast::FunctionDefinition {
1949            name: "inner".to_string(),
1950            body: Box::new(Ast::Sequence(vec![
1951                Ast::Assignment {
1952                    var: "global_var".to_string(),
1953                    value: "inner_modified".to_string(),
1954                },
1955                Ast::Pipeline(vec![ShellCommand {
1956                    args: vec!["printf".to_string(), "inner_done".to_string()],
1957                    input: None,
1958                    output: None,
1959                    append: None,
1960                    here_doc_delimiter: None,
1961                    here_doc_quoted: false,
1962                    here_string_content: None,
1963                }]),
1964            ])),
1965        };
1966
1967        // Define both functions
1968        execute(outer_func, &mut shell_state);
1969        execute(inner_func, &mut shell_state);
1970
1971        // Set initial global value
1972        shell_state.set_var("global_var", "initial".to_string());
1973
1974        // Call outer function (which calls inner function)
1975        let call_ast = Ast::FunctionCall {
1976            name: "outer".to_string(),
1977            args: vec![],
1978        };
1979        let exit_code = execute(call_ast, &mut shell_state);
1980        assert_eq!(exit_code, 0);
1981
1982        // After nested function calls, global variable should be modified by inner function
1983        // (bash behavior: function variable assignments affect global scope)
1984        assert_eq!(
1985            shell_state.get_var("global_var"),
1986            Some("inner_modified".to_string())
1987        );
1988    }
1989
1990    #[test]
1991    fn test_here_string_execution() {
1992        // Test here-string redirection with a simple command
1993        let cmd = ShellCommand {
1994            args: vec!["cat".to_string()],
1995            input: None,
1996            output: None,
1997            append: None,
1998            here_doc_delimiter: None,
1999            here_doc_quoted: false,
2000            here_string_content: Some("hello world".to_string()),
2001        };
2002
2003        // Note: This test would require mocking stdin to provide the here-string content
2004        // For now, we'll just verify the command structure is parsed correctly
2005        assert_eq!(cmd.args, vec!["cat"]);
2006        assert_eq!(cmd.here_string_content, Some("hello world".to_string()));
2007    }
2008
2009    #[test]
2010    fn test_here_document_execution() {
2011        // Test here-document redirection with a simple command
2012        let cmd = ShellCommand {
2013            args: vec!["cat".to_string()],
2014            input: None,
2015            output: None,
2016            append: None,
2017            here_doc_delimiter: Some("EOF".to_string()),
2018            here_doc_quoted: false,
2019            here_string_content: None,
2020        };
2021
2022        // Note: This test would require mocking stdin to provide the here-document content
2023        // For now, we'll just verify the command structure is parsed correctly
2024        assert_eq!(cmd.args, vec!["cat"]);
2025        assert_eq!(cmd.here_doc_delimiter, Some("EOF".to_string()));
2026    }
2027
2028    #[test]
2029    fn test_here_document_with_variable_expansion() {
2030        // Test that variables are expanded in here-document content
2031        let mut shell_state = ShellState::new();
2032        shell_state.set_var("PWD", "/test/path".to_string());
2033
2034        // Simulate here-doc content with variable
2035        let content = "Working dir: $PWD";
2036        let expanded = expand_variables_in_string(content, &mut shell_state);
2037
2038        assert_eq!(expanded, "Working dir: /test/path");
2039    }
2040
2041    #[test]
2042    fn test_here_document_with_command_substitution_builtin() {
2043        // Test that builtin command substitutions work in here-document content
2044        let mut shell_state = ShellState::new();
2045        shell_state.set_var("PWD", "/test/dir".to_string());
2046
2047        // Simulate here-doc content with pwd builtin command substitution
2048        let content = "Current directory: `pwd`";
2049        let expanded = expand_variables_in_string(content, &mut shell_state);
2050
2051        // The pwd builtin should be executed and expanded
2052        assert!(expanded.contains("Current directory: "));
2053    }
2054}