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;