From 9eae49a2b5a993f23d148e8bc499ae55cf405b73 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Sun, 16 Aug 2026 18:05:40 +0100 Subject: [PATCH] fix(parser): honor backslash-newline line continuations in here-documents In a here-document whose delimiter is unquoted, bash removes a backslash-newline pair outright and joins the two lines; a backslash is only literal there when it precedes `$`, a backtick, or another backslash. `heredoc_escape_sequence` covered those three characters but not newline, so the pair fell through to `heredoc_literal_text` and both the backslash and the newline were emitted verbatim. Add an explicit line-continuation rule and exclude it from the literal text rule. A quoted delimiter suppresses expansion entirely, so that path is untouched and continues to preserve the backslash, matching bash. Before: After (and bash): $ cat < alpha \ > alpha \ > beta > beta > EOF > EOF alpha \ alpha beta beta Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- brush-parser/src/word.rs | 9 +++++- brush-shell/tests/cases/compat/here.yaml | 36 +++++++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/brush-parser/src/word.rs b/brush-parser/src/word.rs index 99c4195f6..8819ec0fc 100644 --- a/brush-parser/src/word.rs +++ b/brush-parser/src/word.rs @@ -872,14 +872,21 @@ peg::parser! { legacy_arithmetic_expansion() / command_substitution() / parameter_expansion() / + heredoc_line_continuation() / heredoc_escape_sequence() / heredoc_literal_text() + // In a here-document whose delimiter is unquoted, a backslash-newline pair is a + // line continuation: it is removed outright, joining the two lines. (A delimiter + // that *is* quoted suppresses expansion altogether, so this rule never sees it.) + rule heredoc_line_continuation() -> WordPiece = + "\\\n" { WordPiece::Text(String::new()) } + rule heredoc_escape_sequence() -> WordPiece = s:$("\\" ['$' | '`' | '\\']) { WordPiece::EscapeSequence(s.to_owned()) } rule heredoc_literal_text() -> WordPiece = - s:$((!heredoc_escape_sequence() !dollar_sign_word_piece() [^'`'])+) { + s:$((!heredoc_line_continuation() !heredoc_escape_sequence() !dollar_sign_word_piece() [^'`'])+) { WordPiece::Text(s.to_owned()) } diff --git a/brush-shell/tests/cases/compat/here.yaml b/brush-shell/tests/cases/compat/here.yaml index fe6ecb8fe..32982e0ff 100644 --- a/brush-shell/tests/cases/compat/here.yaml +++ b/brush-shell/tests/cases/compat/here.yaml @@ -306,4 +306,38 @@ cases: i=$((i + 1)) done out=$(cat <<<"$payload") - echo "${#out}" \ No newline at end of file + echo "${#out}" + + - name: "Here doc with line continuation" + stdin: | + cat <