@@ -30,14 +30,25 @@ pub fn to_bash(cmd: &Command) -> String {
3030
3131/// Write a command to the output string
3232fn write_command ( cmd : & Command , out : & mut String ) {
33+ write_command_impl ( cmd, out, true ) ;
34+ }
35+
36+ /// Write a command, optionally including heredoc content
37+ fn write_command_impl ( cmd : & Command , out : & mut String , include_heredoc_content : bool ) {
3338 match cmd {
3439 Command :: Simple {
3540 words,
3641 redirects,
3742 assignments,
3843 ..
3944 } => {
40- write_simple ( words, redirects, assignments. as_deref ( ) , out) ;
45+ write_simple_impl (
46+ words,
47+ redirects,
48+ assignments. as_deref ( ) ,
49+ out,
50+ include_heredoc_content,
51+ ) ;
4152 }
4253 Command :: Pipeline {
4354 commands, negated, ..
@@ -119,11 +130,12 @@ fn write_command(cmd: &Command, out: &mut String) {
119130}
120131
121132/// Write a simple command (cmd arg1 arg2 ...)
122- fn write_simple (
133+ fn write_simple_impl (
123134 words : & [ Word ] ,
124135 redirects : & [ Redirect ] ,
125136 assignments : Option < & [ String ] > ,
126137 out : & mut String ,
138+ include_heredoc_content : bool ,
127139) {
128140 // Check if the command is a builtin that takes assignments as arguments
129141 // (like local, export, declare, readonly, typeset)
@@ -179,12 +191,17 @@ fn write_simple(
179191 }
180192
181193 // Write redirects
182- write_redirects ( redirects, out) ;
194+ write_redirects_impl ( redirects, out, include_heredoc_content ) ;
183195}
184196
185197/// Write multiple redirects
186198/// Heredocs are handled specially - their content comes after all other redirects
187199fn write_redirects ( redirects : & [ Redirect ] , out : & mut String ) {
200+ write_redirects_impl ( redirects, out, true ) ;
201+ }
202+
203+ /// Write redirects, optionally including heredoc content
204+ fn write_redirects_impl ( redirects : & [ Redirect ] , out : & mut String , include_heredoc_content : bool ) {
188205 // First pass: write non-heredoc redirects
189206 for redirect in redirects {
190207 if redirect. direction != RedirectType :: HereDoc {
@@ -199,11 +216,13 @@ fn write_redirects(redirects: &[Redirect], out: &mut String) {
199216 write_heredoc_marker ( redirect, out) ;
200217 }
201218 }
202- // Third pass: write heredoc content (after a newline)
203- for redirect in redirects {
204- if redirect. direction == RedirectType :: HereDoc {
205- out. push ( '\n' ) ;
206- write_heredoc_content ( redirect, out) ;
219+ // Third pass: write heredoc content (after a newline) if requested
220+ if include_heredoc_content {
221+ for redirect in redirects {
222+ if redirect. direction == RedirectType :: HereDoc {
223+ out. push ( '\n' ) ;
224+ write_heredoc_content ( redirect, out) ;
225+ }
207226 }
208227 }
209228}
@@ -329,9 +348,103 @@ fn write_pipeline(commands: &[Command], negated: bool, out: &mut String) {
329348 }
330349}
331350
351+ /// Get the line number of the first token in a command
352+ const fn get_command_line ( cmd : & Command ) -> Option < u32 > {
353+ match cmd {
354+ Command :: Simple { line, .. }
355+ | Command :: Pipeline { line, .. }
356+ | Command :: List { line, .. }
357+ | Command :: For { line, .. }
358+ | Command :: While { line, .. }
359+ | Command :: Until { line, .. }
360+ | Command :: If { line, .. }
361+ | Command :: Case { line, .. }
362+ | Command :: Select { line, .. }
363+ | Command :: Group { line, .. }
364+ | Command :: Subshell { line, .. }
365+ | Command :: FunctionDef { line, .. }
366+ | Command :: Arithmetic { line, .. }
367+ | Command :: ArithmeticFor { line, .. }
368+ | Command :: Conditional { line, .. }
369+ | Command :: Coproc { line, .. } => * line,
370+ }
371+ }
372+
373+ /// Check if a command has any heredoc redirects (requires newline after)
374+ fn has_heredoc ( cmd : & Command ) -> bool {
375+ let redirects = match cmd {
376+ Command :: Simple { redirects, .. }
377+ | Command :: For { redirects, .. }
378+ | Command :: While { redirects, .. }
379+ | Command :: Until { redirects, .. }
380+ | Command :: If { redirects, .. }
381+ | Command :: Case { redirects, .. }
382+ | Command :: Select { redirects, .. }
383+ | Command :: Group { redirects, .. }
384+ | Command :: Subshell { redirects, .. } => redirects,
385+ Command :: List { left, right, .. } => {
386+ return has_heredoc ( left) || has_heredoc ( right) ;
387+ }
388+ Command :: Pipeline { commands, .. } => {
389+ return commands. iter ( ) . any ( has_heredoc) ;
390+ }
391+ _ => return false ,
392+ } ;
393+ redirects
394+ . iter ( )
395+ . any ( |r| r. direction == RedirectType :: HereDoc )
396+ }
397+
398+ /// Collect all heredocs from a command tree
399+ fn collect_heredocs ( cmd : & Command ) -> Vec < & Redirect > {
400+ let mut heredocs = Vec :: new ( ) ;
401+ collect_heredocs_impl ( cmd, & mut heredocs) ;
402+ heredocs
403+ }
404+
405+ fn collect_heredocs_impl < ' a > ( cmd : & ' a Command , heredocs : & mut Vec < & ' a Redirect > ) {
406+ match cmd {
407+ Command :: Simple { redirects, .. }
408+ | Command :: For { redirects, .. }
409+ | Command :: While { redirects, .. }
410+ | Command :: Until { redirects, .. }
411+ | Command :: If { redirects, .. }
412+ | Command :: Case { redirects, .. }
413+ | Command :: Select { redirects, .. }
414+ | Command :: Group { redirects, .. }
415+ | Command :: Subshell { redirects, .. } => {
416+ for r in redirects {
417+ if r. direction == RedirectType :: HereDoc {
418+ heredocs. push ( r) ;
419+ }
420+ }
421+ }
422+ Command :: List { left, right, .. } => {
423+ collect_heredocs_impl ( left, heredocs) ;
424+ collect_heredocs_impl ( right, heredocs) ;
425+ }
426+ Command :: Pipeline { commands, .. } => {
427+ for c in commands {
428+ collect_heredocs_impl ( c, heredocs) ;
429+ }
430+ }
431+ _ => { }
432+ }
433+ }
434+
332435/// Write a list (cmd1 && cmd2, cmd1 || cmd2, etc.)
333436fn write_list ( op : ListOp , left : & Command , right : & Command , out : & mut String ) {
334- write_command ( left, out) ;
437+ // For And/Or with heredocs, we need special handling:
438+ // The heredoc content must come AFTER the entire command line
439+ let left_has_heredoc = has_heredoc ( left) ;
440+ let defer_heredocs = ( op == ListOp :: And || op == ListOp :: Or ) && left_has_heredoc;
441+
442+ if defer_heredocs {
443+ // Write left command without heredoc content
444+ write_command_impl ( left, out, false ) ;
445+ } else {
446+ write_command ( left, out) ;
447+ }
335448
336449 // Check if right side is empty (e.g., "cmd &" has empty right side)
337450 let right_is_empty = matches ! (
@@ -340,9 +453,20 @@ fn write_list(op: ListOp, left: &Command, right: &Command, out: &mut String) {
340453 if words. is_empty( ) && redirects. is_empty( ) && assignments. is_none( )
341454 ) ;
342455
456+ // For semi, use newline if:
457+ // 1. Commands are on different lines, OR
458+ // 2. Left command has a heredoc (heredoc content requires newline after delimiter)
459+ let use_newline = op == ListOp :: Semi
460+ && ( left_has_heredoc
461+ || match ( get_command_line ( left) , get_command_line ( right) ) {
462+ ( Some ( l) , Some ( r) ) => r > l,
463+ _ => false ,
464+ } ) ;
465+
343466 match op {
344467 ListOp :: And => out. push_str ( " && " ) ,
345468 ListOp :: Or => out. push_str ( " || " ) ,
469+ ListOp :: Semi if use_newline => out. push ( '\n' ) ,
346470 ListOp :: Semi => out. push_str ( "; " ) ,
347471 ListOp :: Amp => {
348472 out. push_str ( " &" ) ;
@@ -356,6 +480,14 @@ fn write_list(op: ListOp, left: &Command, right: &Command, out: &mut String) {
356480 if !right_is_empty {
357481 write_command ( right, out) ;
358482 }
483+
484+ // Now write deferred heredoc content
485+ if defer_heredocs {
486+ for heredoc in collect_heredocs ( left) {
487+ out. push ( '\n' ) ;
488+ write_heredoc_content ( heredoc, out) ;
489+ }
490+ }
359491}
360492
361493/// Check if a command ends with a background operator (needs no semicolon after)
0 commit comments