rush_sh/executor/
mod.rs

1//! Command execution engine for the Rush shell.
2//!
3//! This module handles the execution of parsed AST nodes, including pipelines,
4//! control structures, redirections, and built-in commands.
5
6
7use super::parser::Ast;
8use super::state::ShellState;
9
10// Submodules
11mod expansion;
12mod redirection;
13mod command;
14mod subshell;
15mod async_exec;
16
17// Re-export expansion functions
18pub use expansion::expand_variables_in_string;
19
20// Re-export command execution functions
21pub(crate) use command::{execute_and_capture_output, execute_single_command, execute_pipeline};
22
23// Re-export subshell functions
24pub(crate) use subshell::{execute_compound_with_redirections, execute_compound_in_pipeline};
25
26// Re-export async execution functions
27pub(crate) use async_exec::execute_async;
28
29
30/// Execute a trap handler command
31/// Note: Signal masking during trap execution will be added in a future update
32pub fn execute_trap_handler(trap_cmd: &str, shell_state: &mut ShellState) -> i32 {
33    // Save current exit code to preserve it across trap execution
34    let saved_exit_code = shell_state.last_exit_code;
35
36    // TODO: Add signal masking to prevent recursive trap calls
37    // This requires careful handling of the nix sigprocmask API
38    // For now, traps execute without signal masking
39
40    // Parse and execute the trap command
41    let result = match crate::lexer::lex(trap_cmd, shell_state) {
42        Ok(tokens) => {
43            match crate::lexer::expand_aliases(
44                tokens,
45                shell_state,
46                &mut std::collections::HashSet::new(),
47            ) {
48                Ok(expanded_tokens) => {
49                    match crate::parser::parse(expanded_tokens) {
50                        Ok(ast) => execute(ast, shell_state),
51                        Err(_) => {
52                            // Parse error in trap handler - silently continue
53                            saved_exit_code
54                        }
55                    }
56                }
57                Err(_) => {
58                    // Alias expansion error - silently continue
59                    saved_exit_code
60                }
61            }
62        }
63        Err(_) => {
64            // Lex error in trap handler - silently continue
65            saved_exit_code
66        }
67    };
68
69    // Restore the original exit code (trap handlers don't affect $?)
70    shell_state.last_exit_code = saved_exit_code;
71
72    result
73}
74
75/// Evaluate an AST node within the provided shell state and return its exit code.
76///
77/// Executes the given `ast`, updating `shell_state` (variables, loop/function/subshell state,
78/// file descriptor and redirection effects, traps, etc.) as the AST semantics require.
79/// The function returns the final exit code for the executed AST node (0 for success,
80/// non-zero for failure). Side effects on `shell_state` follow the shell semantics
81/// implemented by the executor (variable assignment, function definition/call, loops,
82/// pipelines, redirections, subshell isolation, errexit behavior, traps, etc.).
83///
84/// # Examples
85///
86/// ```
87/// use rush_sh::{Ast, ShellState};
88/// use rush_sh::executor::execute;
89///
90/// let mut state = ShellState::new();
91/// let ast = Ast::Assignment { var: "X".into(), value: "1".into() };
92/// let code = execute(ast, &mut state);
93/// assert_eq!(code, 0);
94/// assert_eq!(state.get_var("X").as_deref(), Some("1"));
95/// ```
96pub fn execute(ast: Ast, shell_state: &mut ShellState) -> i32 {
97    match ast {
98        Ast::Assignment { var, value } => {
99            // Check noexec option (-n): Read commands but don't execute them
100            if shell_state.options.noexec {
101                return 0; // Return success without executing
102            }
103            
104            // Expand variables and command substitutions in the value
105            let expanded_value = expand_variables_in_string(&value, shell_state);
106            shell_state.set_var(&var, expanded_value.clone());
107            
108            // Auto-export if allexport option (-a) is enabled
109            if shell_state.options.allexport {
110                shell_state.export_var(&var);
111            }
112            0
113        }
114        Ast::LocalAssignment { var, value } => {
115            // Check noexec option (-n): Read commands but don't execute them
116            if shell_state.options.noexec {
117                return 0; // Return success without executing
118            }
119            
120            // Expand variables and command substitutions in the value
121            let expanded_value = expand_variables_in_string(&value, shell_state);
122            shell_state.set_local_var(&var, expanded_value);
123            0
124        }
125        Ast::Pipeline(commands) => {
126            if commands.is_empty() {
127                return 0;
128            }
129
130            if commands.len() == 1 {
131                // Single command, handle redirections
132                execute_single_command(&commands[0], shell_state)
133            } else {
134                // Pipeline
135                execute_pipeline(&commands, shell_state)
136            }
137        }
138        Ast::Sequence(asts) => {
139            let mut exit_code = 0;
140            for ast in asts {
141                // Reset last_was_negation flag before executing each command
142                shell_state.last_was_negation = false;
143                
144                exit_code = execute(ast, shell_state);
145
146                // Check if we got an early return from a function
147                if shell_state.is_returning() {
148                    return exit_code;
149                }
150
151                // Check if exit was requested (e.g., from trap handler)
152                if shell_state.exit_requested {
153                    return shell_state.exit_code;
154                }
155
156                // Check for break/continue signals - stop executing remaining statements
157                if shell_state.is_breaking() || shell_state.is_continuing() {
158                    return exit_code;
159                }
160                
161                // Check errexit option (-e): Exit immediately if command fails
162                // POSIX: Don't exit in these contexts:
163                // 1. Inside if/while/until condition (tracked by in_condition flag)
164                // 2. Part of && or || chain (tracked by in_logical_chain flag)
165                // 3. Negated command (tracked by in_negation flag)
166                // 4. Last command was a negation (tracked by last_was_negation flag)
167                if shell_state.options.errexit
168                    && exit_code != 0
169                    && !shell_state.in_condition
170                    && !shell_state.in_logical_chain
171                    && !shell_state.in_negation
172                    && !shell_state.last_was_negation {
173                    // Set exit_requested flag to trigger shell exit
174                    shell_state.exit_requested = true;
175                    shell_state.exit_code = exit_code;
176                    return exit_code;
177                }
178            }
179            exit_code
180        }
181        Ast::If {
182            branches,
183            else_branch,
184        } => {
185            for (condition, then_branch) in branches {
186                // Mark that we're in a condition (for errexit)
187                shell_state.in_condition = true;
188                let cond_exit = execute(*condition, shell_state);
189                shell_state.in_condition = false;
190                
191                if cond_exit == 0 {
192                    let exit_code = execute(*then_branch, shell_state);
193
194                    // Check if we got an early return from a function
195                    if shell_state.is_returning() {
196                        return exit_code;
197                    }
198
199                    return exit_code;
200                }
201            }
202            if let Some(else_b) = else_branch {
203                let exit_code = execute(*else_b, shell_state);
204
205                // Check if we got an early return from a function
206                if shell_state.is_returning() {
207                    return exit_code;
208                }
209
210                exit_code
211            } else {
212                0
213            }
214        }
215        Ast::Case {
216            word,
217            cases,
218            default,
219        } => {
220            for (patterns, branch) in cases {
221                for pattern in &patterns {
222                    if let Ok(glob_pattern) = glob::Pattern::new(pattern) {
223                        if glob_pattern.matches(&word) {
224                            let exit_code = execute(branch, shell_state);
225
226                            // Check if we got an early return from a function
227                            if shell_state.is_returning() {
228                                return exit_code;
229                            }
230
231                            return exit_code;
232                        }
233                    } else {
234                        // If pattern is invalid, fall back to exact match
235                        if &word == pattern {
236                            let exit_code = execute(branch, shell_state);
237
238                            // Check if we got an early return from a function
239                            if shell_state.is_returning() {
240                                return exit_code;
241                            }
242
243                            return exit_code;
244                        }
245                    }
246                }
247            }
248            if let Some(def) = default {
249                let exit_code = execute(*def, shell_state);
250
251                // Check if we got an early return from a function
252                if shell_state.is_returning() {
253                    return exit_code;
254                }
255
256                exit_code
257            } else {
258                0
259            }
260        }
261        Ast::For {
262            variable,
263            items,
264            body,
265        } => {
266            let mut exit_code = 0;
267
268            // Enter loop context
269            shell_state.enter_loop();
270
271            // Expand variables in items and perform word splitting
272            let mut expanded_items = Vec::new();
273            for item in items {
274                // Expand variables in the item
275                let expanded = expand_variables_in_string(&item, shell_state);
276                
277                // Perform word splitting on the expanded result
278                // Split on whitespace (space, tab, newline)
279                for word in expanded.split_whitespace() {
280                    expanded_items.push(word.to_string());
281                }
282            }
283
284            // Execute the loop body for each expanded item
285            for item in expanded_items {
286                // Process any pending signals before executing the body
287                crate::state::process_pending_signals(shell_state);
288
289                // Check if exit was requested (e.g., from trap handler)
290                if shell_state.exit_requested {
291                    shell_state.exit_loop();
292                    return shell_state.exit_code;
293                }
294
295                // Set the loop variable
296                shell_state.set_var(&variable, item.clone());
297
298                // Execute the body
299                exit_code = execute(*body.clone(), shell_state);
300
301                // Check if we got an early return from a function
302                if shell_state.is_returning() {
303                    shell_state.exit_loop();
304                    return exit_code;
305                }
306
307                // Check if exit was requested after executing the body
308                if shell_state.exit_requested {
309                    shell_state.exit_loop();
310                    return shell_state.exit_code;
311                }
312
313                // Check for break signal
314                if shell_state.is_breaking() {
315                    if shell_state.get_break_level() == 1 {
316                        // Break out of this loop
317                        shell_state.clear_break();
318                        break;
319                    } else {
320                        // Decrement level and propagate to outer loop
321                        shell_state.decrement_break_level();
322                        break;
323                    }
324                }
325
326                // Check for continue signal
327                if shell_state.is_continuing() {
328                    if shell_state.get_continue_level() == 1 {
329                        // Continue to next iteration of this loop
330                        shell_state.clear_continue();
331                        continue;
332                    } else {
333                        // Decrement level and propagate to outer loop
334                        shell_state.decrement_continue_level();
335                        break; // Exit this loop to continue outer loop
336                    }
337                }
338            }
339
340            // Exit loop context
341            shell_state.exit_loop();
342
343            exit_code
344        }
345        Ast::While { condition, body } => {
346            let mut exit_code = 0;
347
348            // Enter loop context
349            shell_state.enter_loop();
350
351            // Execute the loop while condition is true (exit code 0)
352            loop {
353                // Mark that we're in a condition (for errexit)
354                shell_state.in_condition = true;
355                let cond_exit = execute(*condition.clone(), shell_state);
356                shell_state.in_condition = false;
357
358                // Check if we got an early return from a function
359                if shell_state.is_returning() {
360                    shell_state.exit_loop();
361                    return cond_exit;
362                }
363
364                // Check if exit was requested (e.g., from trap handler)
365                if shell_state.exit_requested {
366                    shell_state.exit_loop();
367                    return shell_state.exit_code;
368                }
369
370                // If condition is false (non-zero exit code), break
371                if cond_exit != 0 {
372                    break;
373                }
374
375                // Execute the body
376                exit_code = execute(*body.clone(), shell_state);
377
378                // Check if we got an early return from a function
379                if shell_state.is_returning() {
380                    shell_state.exit_loop();
381                    return exit_code;
382                }
383
384                // Check if exit was requested (e.g., from trap handler)
385                if shell_state.exit_requested {
386                    shell_state.exit_loop();
387                    return shell_state.exit_code;
388                }
389
390                // Check for break signal
391                if shell_state.is_breaking() {
392                    if shell_state.get_break_level() == 1 {
393                        // Break out of this loop
394                        shell_state.clear_break();
395                        break;
396                    } else {
397                        // Decrement level and propagate to outer loop
398                        shell_state.decrement_break_level();
399                        break;
400                    }
401                }
402
403                // Check for continue signal
404                if shell_state.is_continuing() {
405                    if shell_state.get_continue_level() == 1 {
406                        // Continue to next iteration of this loop
407                        shell_state.clear_continue();
408                        continue;
409                    } else {
410                        // Decrement level and propagate to outer loop
411                        shell_state.decrement_continue_level();
412                        break; // Exit this loop to continue outer loop
413                    }
414                }
415            }
416
417            // Exit loop context
418            shell_state.exit_loop();
419
420            exit_code
421        }
422        Ast::Until { condition, body } => {
423            let mut exit_code = 0;
424
425            // Enter loop context
426            shell_state.enter_loop();
427
428            // Execute the loop until condition is true (exit code 0)
429            loop {
430                // Mark that we're in a condition (for errexit)
431                shell_state.in_condition = true;
432                let cond_exit = execute(*condition.clone(), shell_state);
433                shell_state.in_condition = false;
434
435                // Check if we got an early return from a function
436                if shell_state.is_returning() {
437                    shell_state.exit_loop();
438                    return cond_exit;
439                }
440
441                // Check if exit was requested (e.g., from trap handler)
442                if shell_state.exit_requested {
443                    shell_state.exit_loop();
444                    return shell_state.exit_code;
445                }
446
447                // If condition is true (exit code 0), break
448                if cond_exit == 0 {
449                    break;
450                }
451
452                // Execute the body
453                exit_code = execute(*body.clone(), shell_state);
454
455                // Check if we got an early return from a function
456                if shell_state.is_returning() {
457                    shell_state.exit_loop();
458                    return exit_code;
459                }
460
461                // Check if exit was requested (e.g., from trap handler)
462                if shell_state.exit_requested {
463                    shell_state.exit_loop();
464                    return shell_state.exit_code;
465                }
466
467                // Check for break signal
468                if shell_state.is_breaking() {
469                    if shell_state.get_break_level() == 1 {
470                        // Break out of this loop
471                        shell_state.clear_break();
472                        break;
473                    } else {
474                        // Decrement level and propagate to outer loop
475                        shell_state.decrement_break_level();
476                        break;
477                    }
478                }
479
480                // Check for continue signal
481                if shell_state.is_continuing() {
482                    if shell_state.get_continue_level() == 1 {
483                        // Continue to next iteration of this loop
484                        shell_state.clear_continue();
485                        continue;
486                    } else {
487                        // Decrement level and propagate to outer loop
488                        shell_state.decrement_continue_level();
489                        break; // Exit this loop to continue outer loop
490                    }
491                }
492            }
493
494            // Exit loop context
495            shell_state.exit_loop();
496
497            exit_code
498        }
499        Ast::FunctionDefinition { name, body } => {
500            // Store function definition in shell state
501            shell_state.define_function(name.clone(), *body);
502            0
503        }
504        Ast::FunctionCall { name, args } => {
505            if let Some(function_body) = shell_state.get_function(&name).cloned() {
506                // Check recursion limit before entering function
507                if shell_state.function_depth >= shell_state.max_recursion_depth {
508                    eprintln!(
509                        "Function recursion limit ({}) exceeded",
510                        shell_state.max_recursion_depth
511                    );
512                    return 1;
513                }
514
515                // Enter function context for local variable scoping
516                shell_state.enter_function();
517
518                // Set up arguments as regular variables (will be enhanced in Phase 2)
519                let old_positional = shell_state.positional_params.clone();
520
521                // Set positional parameters for function arguments
522                shell_state.set_positional_params(args.clone());
523
524                // Save current line number and reset to 1 for function body
525                shell_state.line_number_stack.push(shell_state.current_line_number);
526                shell_state.current_line_number = 1;
527
528                // Execute function body
529                let exit_code = execute(function_body, shell_state);
530
531                // Helper to restore line number from stack
532                let restore_line_number = |state: &mut ShellState| {
533                    if let Some(saved_line) = state.line_number_stack.pop() {
534                        state.current_line_number = saved_line;
535                    }
536                };
537
538                // Check if we got an early return from the function
539                if shell_state.is_returning() {
540                    let return_value = shell_state.get_return_value().unwrap_or(0);
541
542                    // Restore old positional parameters
543                    shell_state.set_positional_params(old_positional);
544
545                    // Restore line number
546                    restore_line_number(shell_state);
547
548                    // Exit function context
549                    shell_state.exit_function();
550
551                    // Clear return state
552                    shell_state.clear_return();
553
554                    // Update last_exit_code so $? captures the return value
555                    shell_state.last_exit_code = return_value;
556
557                    // Return the early return value
558                    return return_value;
559                }
560
561                // Restore old positional parameters
562                shell_state.set_positional_params(old_positional);
563
564                // Restore line number
565                if let Some(saved_line) = shell_state.line_number_stack.pop() {
566                    shell_state.current_line_number = saved_line;
567                }
568
569                // Exit function context
570                shell_state.exit_function();
571
572                // Update last_exit_code so $? captures the function's exit code
573                shell_state.last_exit_code = exit_code;
574
575                exit_code
576            } else {
577                eprintln!("Function '{}' not found", name);
578                1
579            }
580        }
581        Ast::Return { value } => {
582            // Return statements can only be used inside functions
583            if shell_state.function_depth == 0 {
584                eprintln!("Return statement outside of function");
585                return 1;
586            }
587
588            // Parse return value if provided
589            let exit_code = if let Some(ref val) = value {
590                val.parse::<i32>().unwrap_or(0)
591            } else {
592                0
593            };
594
595            // Set return state to indicate early return from function
596            shell_state.set_return(exit_code);
597
598            // Return the exit code - the function call handler will check for this
599            exit_code
600        }
601        Ast::And { left, right } => {
602            // Mark that we're in a logical chain (for errexit)
603            shell_state.in_logical_chain = true;
604            
605            // Execute left side first
606            let left_exit = execute(*left, shell_state);
607
608            // Check ALL control-flow flags after executing left side
609            // If ANY control-flow is active, reset flag and return immediately
610            if shell_state.is_returning()
611                || shell_state.exit_requested
612                || shell_state.is_breaking()
613                || shell_state.is_continuing()
614            {
615                shell_state.in_logical_chain = false;
616                return left_exit;
617            }
618
619            // Only execute right side if left succeeded (exit code 0)
620            let result = if left_exit == 0 {
621                execute(*right, shell_state)
622            } else {
623                left_exit
624            };
625            
626            shell_state.in_logical_chain = false;
627            result
628        }
629        Ast::Or { left, right } => {
630            // Mark that we're in a logical chain (for errexit)
631            shell_state.in_logical_chain = true;
632            
633            // Execute left side first
634            let left_exit = execute(*left, shell_state);
635
636            // Check ALL control-flow flags after executing left side
637            // If ANY control-flow is active, reset flag and return immediately
638            if shell_state.is_returning()
639                || shell_state.exit_requested
640                || shell_state.is_breaking()
641                || shell_state.is_continuing()
642            {
643                shell_state.in_logical_chain = false;
644                return left_exit;
645            }
646
647            // Only execute right side if left failed (exit code != 0)
648            let result = if left_exit != 0 {
649                execute(*right, shell_state)
650            } else {
651                left_exit
652            };
653            
654            shell_state.in_logical_chain = false;
655            result
656        }
657        Ast::Negation { command } => {
658            // Mark that we're in a negation (for errexit)
659            shell_state.in_negation = true;
660            
661            // Execute the negated command
662            let exit_code = execute(*command, shell_state);
663            
664            // Reset negation flag
665            shell_state.in_negation = false;
666            
667            // Mark that this command was a negation (for errexit exemption)
668            shell_state.last_was_negation = true;
669            
670            // Invert the exit code: 0 becomes 1, non-zero becomes 0
671            let inverted_code = if exit_code == 0 { 1 } else { 0 };
672            
673            // Update last_exit_code so $? reflects the inverted code
674            shell_state.last_exit_code = inverted_code;
675            
676            inverted_code
677        }
678        Ast::Subshell { body } => {
679            let exit_code = subshell::execute_subshell(*body, shell_state);
680            
681            // Check errexit option (-e): Exit immediately if subshell fails
682            // POSIX: Don't exit in these contexts:
683            // 1. Inside if/while/until condition (tracked by in_condition flag)
684            // 2. Part of && or || chain (tracked by in_logical_chain flag)
685            // 3. Negated command (tracked by in_negation flag)
686            if shell_state.options.errexit
687                && exit_code != 0
688                && !shell_state.in_condition
689                && !shell_state.in_logical_chain
690                && !shell_state.in_negation {
691                // Set exit_requested flag to trigger shell exit
692                shell_state.exit_requested = true;
693                shell_state.exit_code = exit_code;
694            }
695            
696            exit_code
697        }
698        Ast::CommandGroup { body } => execute(*body, shell_state),
699        Ast::AsyncCommand { command } => {
700            // Execute command asynchronously in the background
701            execute_async(*command, shell_state)
702        }
703    }
704}
705
706#[cfg(test)]
707mod tests;