@@ -271,9 +271,8 @@ pub struct Interpreter {
271271 limits : ExecutionLimits ,
272272 /// Execution counters for resource tracking
273273 counters : ExecutionCounters ,
274- /// Job table for background execution
275- #[ allow( dead_code) ]
276- jobs : JobTable ,
274+ /// Job table for background execution (shared for wait builtin access)
275+ jobs : SharedJobTable ,
277276 /// Shell options (set -e, set -x, etc.)
278277 options : ShellOptions ,
279278 /// Current line number for $LINENO
@@ -545,7 +544,7 @@ impl Interpreter {
545544 call_stack : Vec :: new ( ) ,
546545 limits : ExecutionLimits :: default ( ) ,
547546 counters : ExecutionCounters :: new ( ) ,
548- jobs : JobTable :: new ( ) ,
547+ jobs : jobs :: new_shared_job_table ( ) ,
549548 options : ShellOptions :: default ( ) ,
550549 current_line : 1 ,
551550 #[ cfg( feature = "http_client" ) ]
@@ -3404,42 +3403,104 @@ impl Interpreter {
34043403 Ok ( last_result)
34053404 }
34063405
3406+ /// Check if a command is the empty sentinel produced by the parser for trailing `&`.
3407+ fn is_empty_sentinel ( cmd : & Command ) -> bool {
3408+ if let Command :: Simple ( sc) = cmd {
3409+ let name_is_empty = sc. name . parts . len ( ) == 1
3410+ && matches ! ( & sc. name. parts[ 0 ] , WordPart :: Literal ( s) if s. is_empty( ) ) ;
3411+ name_is_empty
3412+ && sc. args . is_empty ( )
3413+ && sc. redirects . is_empty ( )
3414+ && sc. assignments . is_empty ( )
3415+ } else {
3416+ false
3417+ }
3418+ }
3419+
3420+ /// Run a command as a "background" job.
3421+ ///
3422+ /// Executes the command synchronously (deterministic in virtual env) but
3423+ /// stores the result in the job table so `wait` and `$!` work correctly.
3424+ /// The command's stdout is emitted immediately (like real bash terminal output).
3425+ async fn spawn_in_background (
3426+ & mut self ,
3427+ cmd : & Command ,
3428+ parent_stdout : & mut String ,
3429+ parent_stderr : & mut String ,
3430+ ) -> Result < ( ) > {
3431+ // Execute the command synchronously
3432+ let emit_before = self . output_emit_count ;
3433+ let result = self . execute_command ( cmd) . await ?;
3434+ self . maybe_emit_output ( & result. stdout , & result. stderr , emit_before) ;
3435+
3436+ // Emit output immediately (background output goes to terminal in real bash)
3437+ parent_stdout. push_str ( & result. stdout ) ;
3438+ parent_stderr. push_str ( & result. stderr ) ;
3439+
3440+ // Store only the exit code in the job table (output already emitted)
3441+ let exit_code = result. exit_code ;
3442+ let job_result = ExecResult :: with_code ( String :: new ( ) , exit_code) ;
3443+ let handle = tokio:: spawn ( async move { job_result } ) ;
3444+ let job_id = self . jobs . lock ( ) . await . spawn ( handle) ;
3445+ self . variables
3446+ . insert ( "_LAST_BG_PID" . to_string ( ) , job_id. to_string ( ) ) ;
3447+
3448+ // Background commands always return exit code 0 to the parent
3449+ self . last_exit_code = 0 ;
3450+ // But store the real exit code for $? after wait
3451+ self . variables
3452+ . insert ( "_BG_EXIT_CODE" . to_string ( ) , exit_code. to_string ( ) ) ;
3453+ Ok ( ( ) )
3454+ }
3455+
34073456 /// Execute a command list (cmd1 && cmd2 || cmd3)
3457+ #[ allow( unused_assignments) ] // control_flow may be set but overwritten
34083458 async fn execute_list ( & mut self , list : & CommandList ) -> Result < ExecResult > {
34093459 let mut stdout = String :: new ( ) ;
34103460 let mut stderr = String :: new ( ) ;
34113461 let mut exit_code;
3412- let emit_before = self . output_emit_count ;
3413- let result = self . execute_command ( & list. first ) . await ?;
3414- self . maybe_emit_output ( & result. stdout , & result. stderr , emit_before) ;
3415- stdout. push_str ( & result. stdout ) ;
3416- stderr. push_str ( & result. stderr ) ;
3417- exit_code = result. exit_code ;
3418- self . last_exit_code = exit_code;
3419- let mut control_flow = result. control_flow ;
3462+ let mut control_flow;
34203463
3421- // If first command signaled control flow, return immediately
3422- if control_flow != ControlFlow :: None {
3423- return Ok ( ExecResult {
3424- stdout,
3425- stderr,
3426- exit_code,
3427- control_flow,
3428- } ) ;
3429- }
3464+ // Determine if the first command should run in the background.
3465+ // The `&` terminator for first appears as rest[0].op == Background.
3466+ let first_is_bg = matches ! ( list. rest. first( ) , Some ( ( ListOperator :: Background , _) ) ) ;
34303467
3431- // Check if first command in a semicolon-separated list failed => ERR trap
3432- // Only fire if the first rest operator is semicolon (not &&/||)
3433- let first_op_is_semicolon = list
3434- . rest
3435- . first ( )
3436- . is_some_and ( |( op, _) | matches ! ( op, ListOperator :: Semicolon ) ) ;
3437- if exit_code != 0 && first_op_is_semicolon {
3438- self . run_err_trap ( & mut stdout, & mut stderr) . await ;
3468+ if first_is_bg {
3469+ self . spawn_in_background ( & list. first , & mut stdout, & mut stderr)
3470+ . await ?;
3471+ exit_code = 0 ;
3472+ control_flow = ControlFlow :: None ;
3473+ } else {
3474+ let emit_before = self . output_emit_count ;
3475+ let result = self . execute_command ( & list. first ) . await ?;
3476+ self . maybe_emit_output ( & result. stdout , & result. stderr , emit_before) ;
3477+ stdout. push_str ( & result. stdout ) ;
3478+ stderr. push_str ( & result. stderr ) ;
3479+ exit_code = result. exit_code ;
3480+ self . last_exit_code = exit_code;
3481+ control_flow = result. control_flow ;
3482+
3483+ // If first command signaled control flow, return immediately
3484+ if control_flow != ControlFlow :: None {
3485+ return Ok ( ExecResult {
3486+ stdout,
3487+ stderr,
3488+ exit_code,
3489+ control_flow,
3490+ } ) ;
3491+ }
3492+
3493+ // Check if first command in a semicolon-separated list failed => ERR trap
3494+ let first_op_is_semicolon = list
3495+ . rest
3496+ . first ( )
3497+ . is_some_and ( |( op, _) | matches ! ( op, ListOperator :: Semicolon ) ) ;
3498+ if exit_code != 0 && first_op_is_semicolon {
3499+ self . run_err_trap ( & mut stdout, & mut stderr) . await ;
3500+ }
34393501 }
34403502
34413503 // Track if the list contains any && or || operators
3442- // If so, failures within the list are "handled" by those operators
34433504 let has_conditional_operators = list
34443505 . rest
34453506 . iter ( )
@@ -3449,17 +3510,27 @@ impl Interpreter {
34493510 let mut just_exited_conditional_chain = false ;
34503511
34513512 for ( i, ( op, cmd) ) in list. rest . iter ( ) . enumerate ( ) {
3513+ // Skip empty sentinel commands (produced by trailing `&`)
3514+ if Self :: is_empty_sentinel ( cmd) {
3515+ continue ;
3516+ }
3517+
34523518 // Check if next operator (if any) is && or ||
34533519 let next_op = list. rest . get ( i + 1 ) . map ( |( op, _) | op) ;
34543520 let current_is_conditional = matches ! ( op, ListOperator :: And | ListOperator :: Or ) ;
34553521 let next_is_conditional =
34563522 matches ! ( next_op, Some ( ListOperator :: And ) | Some ( ListOperator :: Or ) ) ;
34573523
3524+ // Determine if THIS command should be backgrounded.
3525+ // A command is backgrounded when the NEXT separator is Background
3526+ // (the `&` terminates the current command).
3527+ let should_background =
3528+ matches ! ( list. rest. get( i + 1 ) , Some ( ( ListOperator :: Background , _) ) ) ;
3529+
34583530 // Check errexit before executing if:
34593531 // - We just exited a conditional chain (and current op is semicolon)
34603532 // - OR: current op is semicolon and previous wasn't in a conditional chain
34613533 // - Exit code is non-zero
3462- // But NOT if we're about to enter/continue a conditional chain
34633534 let should_check_errexit = matches ! ( op, ListOperator :: Semicolon )
34643535 && !just_exited_conditional_chain
34653536 && self . is_errexit_enabled ( )
@@ -3477,55 +3548,51 @@ impl Interpreter {
34773548 // Reset the flag
34783549 just_exited_conditional_chain = false ;
34793550
3480- // Mark that we're exiting a conditional chain if:
3481- // - Current is conditional (&&/||) and next is not conditional (;/end)
3551+ // Mark that we're exiting a conditional chain
34823552 if current_is_conditional && !next_is_conditional {
34833553 just_exited_conditional_chain = true ;
34843554 }
34853555
34863556 let should_execute = match op {
34873557 ListOperator :: And => exit_code == 0 ,
34883558 ListOperator :: Or => exit_code != 0 ,
3489- ListOperator :: Semicolon => true ,
3490- ListOperator :: Background => {
3491- // Background (&) runs command synchronously in virtual mode.
3492- // True process backgrounding requires OS process spawning which
3493- // is excluded from the sandboxed virtual environment by design.
3494- true
3495- }
3559+ ListOperator :: Semicolon | ListOperator :: Background => true ,
34963560 } ;
34973561
34983562 if should_execute {
3499- let emit_before = self . output_emit_count ;
3500- let result = self . execute_command ( cmd) . await ?;
3501- self . maybe_emit_output ( & result. stdout , & result. stderr , emit_before) ;
3502- stdout. push_str ( & result. stdout ) ;
3503- stderr. push_str ( & result. stderr ) ;
3504- exit_code = result. exit_code ;
3505- self . last_exit_code = exit_code;
3506- control_flow = result. control_flow ;
3507-
3508- // If command signaled control flow, return immediately
3509- if control_flow != ControlFlow :: None {
3510- return Ok ( ExecResult {
3511- stdout,
3512- stderr,
3513- exit_code,
3514- control_flow,
3515- } ) ;
3516- }
3563+ if should_background {
3564+ self . spawn_in_background ( cmd, & mut stdout, & mut stderr)
3565+ . await ?;
3566+ exit_code = 0 ;
3567+ } else {
3568+ let emit_before = self . output_emit_count ;
3569+ let result = self . execute_command ( cmd) . await ?;
3570+ self . maybe_emit_output ( & result. stdout , & result. stderr , emit_before) ;
3571+ stdout. push_str ( & result. stdout ) ;
3572+ stderr. push_str ( & result. stderr ) ;
3573+ exit_code = result. exit_code ;
3574+ self . last_exit_code = exit_code;
3575+ control_flow = result. control_flow ;
3576+
3577+ // If command signaled control flow, return immediately
3578+ if control_flow != ControlFlow :: None {
3579+ return Ok ( ExecResult {
3580+ stdout,
3581+ stderr,
3582+ exit_code,
3583+ control_flow,
3584+ } ) ;
3585+ }
35173586
3518- // ERR trap: fire on non-zero exit after semicolon commands (not &&/||)
3519- if exit_code != 0 && !current_is_conditional {
3520- self . run_err_trap ( & mut stdout, & mut stderr) . await ;
3587+ // ERR trap: fire on non-zero exit after semicolon commands
3588+ if exit_code != 0 && !current_is_conditional {
3589+ self . run_err_trap ( & mut stdout, & mut stderr) . await ;
3590+ }
35213591 }
35223592 }
35233593 }
35243594
35253595 // Final errexit check for the last command
3526- // Don't check if:
3527- // - The list had conditional operators (failures are "handled" by && / ||)
3528- // - OR we're in/just exited a conditional chain
35293596 let should_final_errexit_check =
35303597 !has_conditional_operators && self . is_errexit_enabled ( ) && exit_code != 0 ;
35313598
@@ -4034,6 +4101,11 @@ impl Interpreter {
40344101 return self . execute_caller_builtin ( & args, & command. redirects ) . await ;
40354102 }
40364103
4104+ // Handle `wait` - needs direct access to job table
4105+ if name == "wait" {
4106+ return self . execute_wait_builtin ( & args, & command. redirects ) . await ;
4107+ }
4108+
40374109 // Handle `mapfile`/`readarray` - needs direct access to arrays
40384110 if name == "mapfile" || name == "readarray" {
40394111 return self . execute_mapfile ( & args, stdin. as_deref ( ) ) . await ;
@@ -4821,6 +4893,52 @@ impl Interpreter {
48214893 self . apply_redirections ( result, redirects) . await
48224894 }
48234895
4896+ /// Execute the `wait` builtin with direct access to the job table.
4897+ ///
4898+ /// Merges background job stdout/stderr into the result so callers
4899+ /// see the output produced by waited-for jobs.
4900+ async fn execute_wait_builtin (
4901+ & mut self ,
4902+ args : & [ String ] ,
4903+ redirects : & [ Redirect ] ,
4904+ ) -> Result < ExecResult > {
4905+ let mut last_exit_code = 0i32 ;
4906+ let mut stdout = String :: new ( ) ;
4907+ let mut stderr = String :: new ( ) ;
4908+
4909+ if args. is_empty ( ) {
4910+ // Wait for all background jobs, collecting their output
4911+ let results = self . jobs . lock ( ) . await . wait_all_results ( ) . await ;
4912+ for r in results {
4913+ stdout. push_str ( & r. stdout ) ;
4914+ stderr. push_str ( & r. stderr ) ;
4915+ last_exit_code = r. exit_code ;
4916+ }
4917+ } else {
4918+ // Wait for specific job IDs
4919+ for arg in args {
4920+ if let Ok ( job_id) = arg. parse :: < usize > ( )
4921+ && let Some ( r) = self . jobs . lock ( ) . await . wait_for ( job_id) . await
4922+ {
4923+ stdout. push_str ( & r. stdout ) ;
4924+ stderr. push_str ( & r. stderr ) ;
4925+ last_exit_code = r. exit_code ;
4926+ }
4927+ // If job not found or not parseable, that's ok
4928+ }
4929+ }
4930+
4931+ self . last_exit_code = last_exit_code;
4932+ let mut result = ExecResult {
4933+ stdout,
4934+ stderr,
4935+ exit_code : last_exit_code,
4936+ control_flow : ControlFlow :: None ,
4937+ } ;
4938+ result = self . apply_redirections ( result, redirects) . await ?;
4939+ Ok ( result)
4940+ }
4941+
48244942 /// Execute the `alias` builtin. Needs direct access to self.aliases.
48254943 ///
48264944 /// Usage:
0 commit comments