@@ -2741,6 +2741,26 @@ impl Interpreter {
27412741 }
27422742 }
27432743}
2744+
2745+ /// Fd target for redirect fd-table modeling.
2746+ /// Bash processes redirects left-to-right, building an fd table where each
2747+ /// dup copies the *current* target of the source fd. This matters for
2748+ /// patterns like `2>&1 >file` where stderr must capture stdout's original
2749+ /// destination before stdout is redirected to the file.
2750+ #[ derive( Clone , Debug ) ]
2751+ enum FdTarget {
2752+ /// The original stdout pipe (terminal / command-substitution capture).
2753+ Stdout ,
2754+ /// The original stderr pipe.
2755+ Stderr ,
2756+ /// Write (truncate) to a file.
2757+ WriteFile ( PathBuf , String ) ,
2758+ /// Append to a file.
2759+ AppendFile ( PathBuf , String ) ,
2760+ /// Discard (/dev/null).
2761+ DevNull ,
2762+ }
2763+
27442764impl Interpreter {
27452765 /// Execute a sequence of commands (with errexit checking)
27462766 async fn execute_command_sequence ( & mut self , commands : & [ Command ] ) -> Result < ExecResult > {
@@ -5437,20 +5457,36 @@ impl Interpreter {
54375457 mut result : ExecResult ,
54385458 redirects : & [ Redirect ] ,
54395459 ) -> Result < ExecResult > {
5460+ // Skip the fd-table path when there are no DupOutput redirects mixed
5461+ // with file redirects — the simple single-pass logic is sufficient and
5462+ // avoids any behavioural delta for the common case.
5463+ let has_dup_output = redirects. iter ( ) . any ( |r| r. kind == RedirectKind :: DupOutput ) ;
5464+ let has_file_redirect = redirects. iter ( ) . any ( |r| {
5465+ matches ! (
5466+ r. kind,
5467+ RedirectKind :: Output
5468+ | RedirectKind :: Clobber
5469+ | RedirectKind :: Append
5470+ | RedirectKind :: OutputBoth
5471+ )
5472+ } ) ;
5473+
5474+ if has_dup_output && has_file_redirect {
5475+ return self . apply_redirections_fd_table ( result, redirects) . await ;
5476+ }
5477+
5478+ // --- Fast path: no mixed dup+file redirects ---
54405479 for redirect in redirects {
54415480 match redirect. kind {
54425481 RedirectKind :: Output | RedirectKind :: Clobber => {
54435482 let target_path = self . expand_word ( & redirect. target ) . await ?;
54445483 let path = self . resolve_path ( & target_path) ;
5445- // Handle /dev/null at interpreter level - cannot be bypassed
54465484 if is_dev_null ( & path) {
5447- // Discard output without calling filesystem
54485485 match redirect. fd {
54495486 Some ( 2 ) => result. stderr = String :: new ( ) ,
54505487 _ => result. stdout = String :: new ( ) ,
54515488 }
54525489 } else {
5453- // noclobber check: > fails if file exists (>| bypasses)
54545490 if redirect. kind == RedirectKind :: Output
54555491 && self . variables . get ( "SHOPT_C" ) . map ( |v| v. as_str ( ) ) == Some ( "1" )
54565492 && self . fs . stat ( & path) . await . is_ok ( )
@@ -5461,26 +5497,21 @@ impl Interpreter {
54615497 result. exit_code = 1 ;
54625498 return Ok ( result) ;
54635499 }
5464- // Check which fd we're redirecting
54655500 match redirect. fd {
54665501 Some ( 2 ) => {
5467- // 2> - redirect stderr to file
54685502 if let Err ( e) =
54695503 self . fs . write_file ( & path, result. stderr . as_bytes ( ) ) . await
54705504 {
5471- // Redirect failed - set exit code and report error
54725505 result. stderr = format ! ( "bash: {}: {}\n " , target_path, e) ;
54735506 result. exit_code = 1 ;
54745507 return Ok ( result) ;
54755508 }
54765509 result. stderr = String :: new ( ) ;
54775510 }
54785511 _ => {
5479- // Default (stdout) - write stdout to file
54805512 if let Err ( e) =
54815513 self . fs . write_file ( & path, result. stdout . as_bytes ( ) ) . await
54825514 {
5483- // Redirect failed - output is lost, set exit code and report error
54845515 result. stdout = String :: new ( ) ;
54855516 result. stderr = format ! ( "bash: {}: {}\n " , target_path, e) ;
54865517 result. exit_code = 1 ;
@@ -5494,18 +5525,14 @@ impl Interpreter {
54945525 RedirectKind :: Append => {
54955526 let target_path = self . expand_word ( & redirect. target ) . await ?;
54965527 let path = self . resolve_path ( & target_path) ;
5497- // Handle /dev/null at interpreter level - cannot be bypassed
54985528 if is_dev_null ( & path) {
5499- // Discard output without calling filesystem
55005529 match redirect. fd {
55015530 Some ( 2 ) => result. stderr = String :: new ( ) ,
55025531 _ => result. stdout = String :: new ( ) ,
55035532 }
55045533 } else {
5505- // Check which fd we're appending
55065534 match redirect. fd {
55075535 Some ( 2 ) => {
5508- // 2>> - append stderr to file
55095536 if let Err ( e) =
55105537 self . fs . append_file ( & path, result. stderr . as_bytes ( ) ) . await
55115538 {
@@ -5516,11 +5543,9 @@ impl Interpreter {
55165543 result. stderr = String :: new ( ) ;
55175544 }
55185545 _ => {
5519- // Default (stdout) - append stdout to file
55205546 if let Err ( e) =
55215547 self . fs . append_file ( & path, result. stdout . as_bytes ( ) ) . await
55225548 {
5523- // Redirect failed - output is lost
55245549 result. stdout = String :: new ( ) ;
55255550 result. stderr = format ! ( "bash: {}: {}\n " , target_path, e) ;
55265551 result. exit_code = 1 ;
@@ -5532,16 +5557,12 @@ impl Interpreter {
55325557 }
55335558 }
55345559 RedirectKind :: OutputBoth => {
5535- // &> - redirect both stdout and stderr to file
55365560 let target_path = self . expand_word ( & redirect. target ) . await ?;
55375561 let path = self . resolve_path ( & target_path) ;
5538- // Handle /dev/null at interpreter level - cannot be bypassed
55395562 if is_dev_null ( & path) {
5540- // Discard both outputs without calling filesystem
55415563 result. stdout = String :: new ( ) ;
55425564 result. stderr = String :: new ( ) ;
55435565 } else {
5544- // Write both stdout and stderr to file
55455566 let combined = format ! ( "{}{}" , result. stdout, result. stderr) ;
55465567 if let Err ( e) = self . fs . write_file ( & path, combined. as_bytes ( ) ) . await {
55475568 result. stderr = format ! ( "bash: {}: {}\n " , target_path, e) ;
@@ -5553,40 +5574,170 @@ impl Interpreter {
55535574 }
55545575 }
55555576 RedirectKind :: DupOutput => {
5556- // Handle fd duplication (e.g., 2>&1, >&2)
55575577 let target = self . expand_word ( & redirect. target ) . await ?;
55585578 let target_fd: i32 = target. parse ( ) . unwrap_or ( 1 ) ;
55595579 let src_fd = redirect. fd . unwrap_or ( 1 ) ;
55605580
55615581 match ( src_fd, target_fd) {
55625582 ( 2 , 1 ) => {
5563- // 2>&1 - redirect stderr to stdout
55645583 result. stdout . push_str ( & result. stderr ) ;
55655584 result. stderr = String :: new ( ) ;
55665585 }
55675586 ( 1 , 2 ) => {
5568- // >&2 or 1>&2 - redirect stdout to stderr
55695587 result. stderr . push_str ( & result. stdout ) ;
55705588 result. stdout = String :: new ( ) ;
55715589 }
5572- _ => {
5573- // Other fd duplications not yet supported
5574- }
5590+ _ => { }
55755591 }
55765592 }
55775593 RedirectKind :: Input
55785594 | RedirectKind :: HereString
55795595 | RedirectKind :: HereDoc
5580- | RedirectKind :: HereDocStrip => {
5581- // Input redirections handled in process_input_redirections
5596+ | RedirectKind :: HereDocStrip => { }
5597+ RedirectKind :: DupInput => { }
5598+ }
5599+ }
5600+
5601+ Ok ( result)
5602+ }
5603+
5604+ /// Apply redirections using an fd-table model for correct left-to-right
5605+ /// ordering when DupOutput and file redirects are mixed (e.g. `2>&1 >file`).
5606+ async fn apply_redirections_fd_table (
5607+ & mut self ,
5608+ mut result : ExecResult ,
5609+ redirects : & [ Redirect ] ,
5610+ ) -> Result < ExecResult > {
5611+ // Build fd table: fd1 = stdout pipe, fd2 = stderr pipe
5612+ let mut fd1 = FdTarget :: Stdout ;
5613+ let mut fd2 = FdTarget :: Stderr ;
5614+
5615+ for redirect in redirects {
5616+ match redirect. kind {
5617+ RedirectKind :: Output | RedirectKind :: Clobber => {
5618+ let target_path = self . expand_word ( & redirect. target ) . await ?;
5619+ let path = self . resolve_path ( & target_path) ;
5620+
5621+ if redirect. kind == RedirectKind :: Output
5622+ && self . variables . get ( "SHOPT_C" ) . map ( |v| v. as_str ( ) ) == Some ( "1" )
5623+ && !is_dev_null ( & path)
5624+ && self . fs . stat ( & path) . await . is_ok ( )
5625+ {
5626+ result. stdout = String :: new ( ) ;
5627+ result. stderr =
5628+ format ! ( "bash: {}: cannot overwrite existing file\n " , target_path) ;
5629+ result. exit_code = 1 ;
5630+ return Ok ( result) ;
5631+ }
5632+
5633+ let target = if is_dev_null ( & path) {
5634+ FdTarget :: DevNull
5635+ } else {
5636+ FdTarget :: WriteFile ( path, target_path)
5637+ } ;
5638+ match redirect. fd {
5639+ Some ( 2 ) => fd2 = target,
5640+ _ => fd1 = target,
5641+ }
55825642 }
5583- RedirectKind :: DupInput => {
5584- // Input fd duplication - handled in process_input_redirections
5585- // for coproc FDs; other cases not yet supported
5643+ RedirectKind :: Append => {
5644+ let target_path = self . expand_word ( & redirect. target ) . await ?;
5645+ let path = self . resolve_path ( & target_path) ;
5646+ let target = if is_dev_null ( & path) {
5647+ FdTarget :: DevNull
5648+ } else {
5649+ FdTarget :: AppendFile ( path, target_path)
5650+ } ;
5651+ match redirect. fd {
5652+ Some ( 2 ) => fd2 = target,
5653+ _ => fd1 = target,
5654+ }
5655+ }
5656+ RedirectKind :: OutputBoth => {
5657+ let target_path = self . expand_word ( & redirect. target ) . await ?;
5658+ let path = self . resolve_path ( & target_path) ;
5659+ let target = if is_dev_null ( & path) {
5660+ FdTarget :: DevNull
5661+ } else {
5662+ FdTarget :: WriteFile ( path, target_path)
5663+ } ;
5664+ fd1 = target. clone ( ) ;
5665+ fd2 = target;
5666+ }
5667+ RedirectKind :: DupOutput => {
5668+ let target = self . expand_word ( & redirect. target ) . await ?;
5669+ let target_fd: i32 = target. parse ( ) . unwrap_or ( 1 ) ;
5670+ let src_fd = redirect. fd . unwrap_or ( 1 ) ;
5671+
5672+ match ( src_fd, target_fd) {
5673+ ( 2 , 1 ) => fd2 = fd1. clone ( ) ,
5674+ ( 1 , 2 ) => fd1 = fd2. clone ( ) ,
5675+ _ => { }
5676+ }
55865677 }
5678+ RedirectKind :: Input
5679+ | RedirectKind :: HereString
5680+ | RedirectKind :: HereDoc
5681+ | RedirectKind :: HereDocStrip
5682+ | RedirectKind :: DupInput => { }
5683+ }
5684+ }
5685+
5686+ // Route original stdout/stderr based on final fd table.
5687+ // Collect file writes to avoid double-borrow issues.
5688+ let orig_stdout = std:: mem:: take ( & mut result. stdout ) ;
5689+ let orig_stderr = std:: mem:: take ( & mut result. stderr ) ;
5690+
5691+ // Determine what goes where
5692+ let mut new_stdout = String :: new ( ) ;
5693+ let mut new_stderr = String :: new ( ) ;
5694+ // file_path -> (content, is_append, display_path)
5695+ let mut file_writes: std:: collections:: HashMap < PathBuf , ( String , bool , String ) > =
5696+ std:: collections:: HashMap :: new ( ) ;
5697+
5698+ for ( data, fd_target) in [ ( & orig_stdout, & fd1) , ( & orig_stderr, & fd2) ] . iter ( ) {
5699+ if data. is_empty ( ) {
5700+ continue ;
5701+ }
5702+ match fd_target {
5703+ FdTarget :: Stdout => new_stdout. push_str ( data) ,
5704+ FdTarget :: Stderr => new_stderr. push_str ( data) ,
5705+ FdTarget :: DevNull => { }
5706+ FdTarget :: WriteFile ( path, display) => {
5707+ file_writes
5708+ . entry ( path. clone ( ) )
5709+ . or_insert_with ( || ( String :: new ( ) , false , display. clone ( ) ) )
5710+ . 0
5711+ . push_str ( data) ;
5712+ }
5713+ FdTarget :: AppendFile ( path, display) => {
5714+ file_writes
5715+ . entry ( path. clone ( ) )
5716+ . or_insert_with ( || ( String :: new ( ) , true , display. clone ( ) ) )
5717+ . 0
5718+ . push_str ( data) ;
5719+ }
5720+ }
5721+ }
5722+
5723+ // Write files
5724+ for ( path, ( content, is_append, display_path) ) in & file_writes {
5725+ let write_result = if * is_append {
5726+ self . fs . append_file ( path, content. as_bytes ( ) ) . await
5727+ } else {
5728+ self . fs . write_file ( path, content. as_bytes ( ) ) . await
5729+ } ;
5730+ if let Err ( e) = write_result {
5731+ new_stderr = format ! ( "bash: {}: {}\n " , display_path, e) ;
5732+ result. exit_code = 1 ;
5733+ result. stdout = new_stdout;
5734+ result. stderr = new_stderr;
5735+ return Ok ( result) ;
55875736 }
55885737 }
55895738
5739+ result. stdout = new_stdout;
5740+ result. stderr = new_stderr;
55905741 Ok ( result)
55915742 }
55925743
0 commit comments