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