@@ -5436,6 +5436,174 @@ impl Interpreter {
54365436 & mut self ,
54375437 mut result : ExecResult ,
54385438 redirects : & [ Redirect ] ,
5439+ ) -> Result < ExecResult > {
5440+ // Model fd destinations to handle left-to-right evaluation order correctly.
5441+ // In bash, `2>&1 >file` means: fd2 duplicates fd1's current target (stdout),
5442+ // THEN fd1 is redirected to file. So fd2->stdout, fd1->file.
5443+ // We track where each fd points and resolve content routing at the end.
5444+
5445+ // Use fd-table modeling when DupOutput coexists with file redirects.
5446+ // Without this, `>file 2>&1` or `2>&1 >file` would route content
5447+ // incorrectly because simple sequential application can't track
5448+ // that a dup should alias the file target rather than the buffer.
5449+ let has_dup = redirects
5450+ . iter ( )
5451+ . any ( |r| matches ! ( r. kind, RedirectKind :: DupOutput ) ) ;
5452+ let has_file = redirects. iter ( ) . any ( |r| {
5453+ matches ! (
5454+ r. kind,
5455+ RedirectKind :: Output
5456+ | RedirectKind :: Clobber
5457+ | RedirectKind :: Append
5458+ | RedirectKind :: OutputBoth
5459+ )
5460+ } ) ;
5461+ let needs_fd_table = has_dup && has_file;
5462+
5463+ if !needs_fd_table {
5464+ // Fast path: no dup-then-redirect combos, use simple direct application.
5465+ return self . apply_redirections_simple ( result, redirects) . await ;
5466+ }
5467+
5468+ // Fd routing table: track where fd1 (stdout) and fd2 (stderr) point.
5469+ #[ derive( Clone , Debug ) ]
5470+ enum FdTarget {
5471+ /// The original stdout buffer (returned in result.stdout)
5472+ Stdout ,
5473+ /// The original stderr buffer (returned in result.stderr)
5474+ Stderr ,
5475+ /// Write to a file (path, display_name, append)
5476+ File ( PathBuf , String , bool ) ,
5477+ /// Discard output
5478+ DevNull ,
5479+ }
5480+
5481+ let mut fd1_target = FdTarget :: Stdout ;
5482+ let mut fd2_target = FdTarget :: Stderr ;
5483+
5484+ // Build the fd routing table by processing redirects left-to-right.
5485+ for redirect in redirects {
5486+ match redirect. kind {
5487+ RedirectKind :: Output | RedirectKind :: Clobber => {
5488+ let target_path = self . expand_word ( & redirect. target ) . await ?;
5489+ let path = self . resolve_path ( & target_path) ;
5490+ if is_dev_null ( & path) {
5491+ match redirect. fd {
5492+ Some ( 2 ) => fd2_target = FdTarget :: DevNull ,
5493+ _ => fd1_target = FdTarget :: DevNull ,
5494+ }
5495+ } else {
5496+ // noclobber check
5497+ if redirect. kind == RedirectKind :: Output
5498+ && self . variables . get ( "SHOPT_C" ) . map ( |v| v. as_str ( ) ) == Some ( "1" )
5499+ && self . fs . stat ( & path) . await . is_ok ( )
5500+ {
5501+ result. stdout = String :: new ( ) ;
5502+ result. stderr =
5503+ format ! ( "bash: {}: cannot overwrite existing file\n " , target_path) ;
5504+ result. exit_code = 1 ;
5505+ return Ok ( result) ;
5506+ }
5507+ let target = FdTarget :: File ( path, target_path, false ) ;
5508+ match redirect. fd {
5509+ Some ( 2 ) => fd2_target = target,
5510+ _ => fd1_target = target,
5511+ }
5512+ }
5513+ }
5514+ RedirectKind :: Append => {
5515+ let target_path = self . expand_word ( & redirect. target ) . await ?;
5516+ let path = self . resolve_path ( & target_path) ;
5517+ if is_dev_null ( & path) {
5518+ match redirect. fd {
5519+ Some ( 2 ) => fd2_target = FdTarget :: DevNull ,
5520+ _ => fd1_target = FdTarget :: DevNull ,
5521+ }
5522+ } else {
5523+ let target = FdTarget :: File ( path, target_path, true ) ;
5524+ match redirect. fd {
5525+ Some ( 2 ) => fd2_target = target,
5526+ _ => fd1_target = target,
5527+ }
5528+ }
5529+ }
5530+ RedirectKind :: OutputBoth => {
5531+ let target_path = self . expand_word ( & redirect. target ) . await ?;
5532+ let path = self . resolve_path ( & target_path) ;
5533+ if is_dev_null ( & path) {
5534+ fd1_target = FdTarget :: DevNull ;
5535+ fd2_target = FdTarget :: DevNull ;
5536+ } else {
5537+ fd1_target = FdTarget :: File ( path. clone ( ) , target_path. clone ( ) , false ) ;
5538+ fd2_target = FdTarget :: File ( path, target_path, false ) ;
5539+ }
5540+ }
5541+ RedirectKind :: DupOutput => {
5542+ let target = self . expand_word ( & redirect. target ) . await ?;
5543+ let target_fd: i32 = target. parse ( ) . unwrap_or ( 1 ) ;
5544+ let src_fd = redirect. fd . unwrap_or ( 1 ) ;
5545+ // Duplicate: make src_fd point where target_fd currently points
5546+ match ( src_fd, target_fd) {
5547+ ( 2 , 1 ) => fd2_target = fd1_target. clone ( ) ,
5548+ ( 1 , 2 ) => fd1_target = fd2_target. clone ( ) ,
5549+ _ => { }
5550+ }
5551+ }
5552+ RedirectKind :: Input
5553+ | RedirectKind :: HereString
5554+ | RedirectKind :: HereDoc
5555+ | RedirectKind :: HereDocStrip
5556+ | RedirectKind :: DupInput => {
5557+ // Input redirections handled elsewhere
5558+ }
5559+ }
5560+ }
5561+
5562+ // Route original stdout/stderr content to final destinations.
5563+ let orig_stdout = std:: mem:: take ( & mut result. stdout ) ;
5564+ let orig_stderr = std:: mem:: take ( & mut result. stderr ) ;
5565+
5566+ // Collect file writes to handle same-file merging.
5567+ let mut file_writes: std:: collections:: HashMap < PathBuf , ( String , String , bool ) > =
5568+ std:: collections:: HashMap :: new ( ) ;
5569+
5570+ for ( content, target) in [ ( & orig_stdout, & fd1_target) , ( & orig_stderr, & fd2_target) ] {
5571+ match target {
5572+ FdTarget :: Stdout => result. stdout . push_str ( content) ,
5573+ FdTarget :: Stderr => result. stderr . push_str ( content) ,
5574+ FdTarget :: DevNull => { }
5575+ FdTarget :: File ( path, display_name, append) => {
5576+ let entry = file_writes
5577+ . entry ( path. clone ( ) )
5578+ . or_insert_with ( || ( String :: new ( ) , display_name. clone ( ) , * append) ) ;
5579+ entry. 0 . push_str ( content) ;
5580+ }
5581+ }
5582+ }
5583+
5584+ // Write accumulated file content
5585+ for ( path, ( content, display_name, append) ) in & file_writes {
5586+ let write_result = if * append {
5587+ self . fs . append_file ( path, content. as_bytes ( ) ) . await
5588+ } else {
5589+ self . fs . write_file ( path, content. as_bytes ( ) ) . await
5590+ } ;
5591+ if let Err ( e) = write_result {
5592+ result. stderr = format ! ( "bash: {}: {}\n " , display_name, e) ;
5593+ result. exit_code = 1 ;
5594+ return Ok ( result) ;
5595+ }
5596+ }
5597+
5598+ Ok ( result)
5599+ }
5600+
5601+ /// Simple redirect application without fd-table modeling.
5602+ /// Used when there are no DupOutput redirects followed by other redirects.
5603+ async fn apply_redirections_simple (
5604+ & mut self ,
5605+ mut result : ExecResult ,
5606+ redirects : & [ Redirect ] ,
54395607 ) -> Result < ExecResult > {
54405608 for redirect in redirects {
54415609 match redirect. kind {
0 commit comments