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