@@ -475,6 +475,9 @@ pub struct Interpreter {
475475 coproc_buffers : HashMap < i32 , Vec < String > > ,
476476 /// Next virtual FD to assign for coproc read ends (starts at 63, like bash).
477477 coproc_next_fd : i32 ,
478+ /// Persistent fd output table set by `exec N>/path` redirections.
479+ /// Maps fd number to its output target. Used by `>&N` redirections.
480+ exec_fd_table : HashMap < i32 , FdTarget > ,
478481 /// Cancellation token: when set to `true`, execution aborts at the next
479482 /// command boundary with `Error::Cancelled`.
480483 cancelled : Arc < AtomicBool > ,
@@ -783,6 +786,7 @@ impl Interpreter {
783786 subst_generation : 0 ,
784787 coproc_buffers : HashMap :: new ( ) ,
785788 coproc_next_fd : 63 ,
789+ exec_fd_table : HashMap :: new ( ) ,
786790 cancelled : Arc :: new ( AtomicBool :: new ( false ) ) ,
787791 deferred_proc_subs : Vec :: new ( ) ,
788792 }
@@ -3668,6 +3672,51 @@ impl Interpreter {
36683672 self . coproc_buffers . remove ( & fd) ;
36693673 }
36703674 }
3675+ RedirectKind :: Output | RedirectKind :: Clobber => {
3676+ let fd = redirect. fd . unwrap_or ( 1 ) ;
3677+ let target_path = self . expand_word ( & redirect. target ) . await ?;
3678+ let path = self . resolve_path ( & target_path) ;
3679+ if is_dev_null ( & path) {
3680+ self . exec_fd_table . insert ( fd, FdTarget :: DevNull ) ;
3681+ } else {
3682+ // Truncate file on open (like real exec >file)
3683+ let _ = self . fs . write_file ( & path, b"" ) . await ;
3684+ self . exec_fd_table
3685+ . insert ( fd, FdTarget :: WriteFile ( path, target_path) ) ;
3686+ }
3687+ }
3688+ RedirectKind :: Append => {
3689+ let fd = redirect. fd . unwrap_or ( 1 ) ;
3690+ let target_path = self . expand_word ( & redirect. target ) . await ?;
3691+ let path = self . resolve_path ( & target_path) ;
3692+ if is_dev_null ( & path) {
3693+ self . exec_fd_table . insert ( fd, FdTarget :: DevNull ) ;
3694+ } else {
3695+ self . exec_fd_table
3696+ . insert ( fd, FdTarget :: AppendFile ( path, target_path) ) ;
3697+ }
3698+ }
3699+ RedirectKind :: DupOutput => {
3700+ let target = self . expand_word ( & redirect. target ) . await ?;
3701+ let fd = redirect. fd . unwrap_or ( 1 ) ;
3702+ if target == "-" {
3703+ // exec N>&- closes the fd
3704+ self . exec_fd_table . remove ( & fd) ;
3705+ } else if let Ok ( target_fd) = target. parse :: < i32 > ( ) {
3706+ // exec N>&M duplicates fd M to fd N
3707+ let target_entry = if target_fd == 1 {
3708+ FdTarget :: Stdout
3709+ } else if target_fd == 2 {
3710+ FdTarget :: Stderr
3711+ } else {
3712+ self . exec_fd_table
3713+ . get ( & target_fd)
3714+ . cloned ( )
3715+ . unwrap_or ( FdTarget :: Stdout )
3716+ } ;
3717+ self . exec_fd_table . insert ( fd, target_entry) ;
3718+ }
3719+ }
36713720 _ => { }
36723721 }
36733722 }
@@ -5625,16 +5674,33 @@ impl Interpreter {
56255674 let target_fd: i32 = target. parse ( ) . unwrap_or ( 1 ) ;
56265675 let src_fd = redirect. fd . unwrap_or ( 1 ) ;
56275676
5628- match ( src_fd, target_fd) {
5629- ( 2 , 1 ) => {
5630- result. stdout . push_str ( & result. stderr ) ;
5631- result. stderr = String :: new ( ) ;
5677+ // Check exec_fd_table for persistent fd targets
5678+ if let Some ( fd_target) = self . exec_fd_table . get ( & target_fd) . cloned ( ) {
5679+ let data = if src_fd == 2 {
5680+ std:: mem:: take ( & mut result. stderr )
5681+ } else {
5682+ std:: mem:: take ( & mut result. stdout )
5683+ } ;
5684+ match & fd_target {
5685+ FdTarget :: Stdout => result. stdout . push_str ( & data) ,
5686+ FdTarget :: Stderr => result. stderr . push_str ( & data) ,
5687+ FdTarget :: DevNull => { }
5688+ FdTarget :: WriteFile ( path, _) | FdTarget :: AppendFile ( path, _) => {
5689+ self . fs . append_file ( path, data. as_bytes ( ) ) . await ?;
5690+ }
56325691 }
5633- ( 1 , 2 ) => {
5634- result. stderr . push_str ( & result. stdout ) ;
5635- result. stdout = String :: new ( ) ;
5692+ } else {
5693+ match ( src_fd, target_fd) {
5694+ ( 2 , 1 ) => {
5695+ result. stdout . push_str ( & result. stderr ) ;
5696+ result. stderr = String :: new ( ) ;
5697+ }
5698+ ( 1 , 2 ) => {
5699+ result. stderr . push_str ( & result. stdout ) ;
5700+ result. stdout = String :: new ( ) ;
5701+ }
5702+ _ => { }
56365703 }
5637- _ => { }
56385704 }
56395705 }
56405706 RedirectKind :: Input
@@ -5716,10 +5782,18 @@ impl Interpreter {
57165782 let target_fd: i32 = target. parse ( ) . unwrap_or ( 1 ) ;
57175783 let src_fd = redirect. fd . unwrap_or ( 1 ) ;
57185784
5719- match ( src_fd, target_fd) {
5720- ( 2 , 1 ) => fd2 = fd1. clone ( ) ,
5721- ( 1 , 2 ) => fd1 = fd2. clone ( ) ,
5722- _ => { }
5785+ // Look up exec_fd_table for persistent fd targets
5786+ if let Some ( exec_target) = self . exec_fd_table . get ( & target_fd) . cloned ( ) {
5787+ match src_fd {
5788+ 2 => fd2 = exec_target,
5789+ _ => fd1 = exec_target,
5790+ }
5791+ } else {
5792+ match ( src_fd, target_fd) {
5793+ ( 2 , 1 ) => fd2 = fd1. clone ( ) ,
5794+ ( 1 , 2 ) => fd1 = fd2. clone ( ) ,
5795+ _ => { }
5796+ }
57235797 }
57245798 }
57255799 RedirectKind :: Input
0 commit comments