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;