Skip to content

fix: stop heredoc delimiter from absorbing trailing control operators - #1117

Closed
daewoongoh wants to merge 2 commits into
Zoo-Code-Org:mainfrom
daewoongoh:fix/heredoc-delimiter-parsing
Closed

fix: stop heredoc delimiter from absorbing trailing control operators#1117
daewoongoh wants to merge 2 commits into
Zoo-Code-Org:mainfrom
daewoongoh:fix/heredoc-delimiter-parsing

Conversation

@daewoongoh

@daewoongoh daewoongoh commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

A heredoc delimiter is a single shell word, so a control operator (;, |, &&) on the opener line terminates the word and must not be absorbed into the delimiter. Previously the parser kept scanning past these operators, so the terminator line was never found and a valid command was incorrectly rejected as an unterminated heredoc.

Add isHeredocDelimiterTerminator() to stop the delimiter at POSIX control operators, matching shell tokenization, and apply it to both the backslash-escaped and unquoted delimiter branches. Add regression tests covering ;, |, and && on the opener line.

Related GitHub Issue

Closes: #1116

Description

The bug: When a heredoc opener line is followed by a control operator on the same line (e.g. << EOF; echo done), the parser absorbed the control operator into the heredoc delimiter. Because a heredoc delimiter is a single shell word, POSIX shell tokenization terminates the word at a control operator — but the parser kept scanning past ;, |, &&, etc. As a result, the terminator line was never found and a valid command was incorrectly rejected as an unterminated heredoc.

The fix: Added a small helper isHeredocDelimiterTerminator() in parse-command.ts that stops the delimiter at POSIX control operators (; | & > < ( )) in addition to whitespace/newline, matching shell tokenization. It is applied to both the backslash-escaped and unquoted delimiter branches.

Key design choices / trade-offs:

  • The fix is minimal and preserves the existing behavior of treating the whole heredoc as a single opaque token (so body lines are never split into independently auto-approved sub-commands).
  • Quoted delimiters (<< 'EOF', << "EOF") are intentionally unaffected — characters inside quotes are literal and not control operators.

What reviewers should pay attention to:

  • The > and < additions to the terminator set — these are valid POSIX control operators and align with shell tokenization, but confirm no regression for edge-case delimiters.
  • The & addition — & is a control operator, so a delimiter like A&B is now split, which matches the shell.

Test Procedure

How I tested:

  • Added regression tests in parse-command.spec.ts covering all three control operators on the opener line: ;, |, and &&.
  • Ran the full test file: cd src && npx vitest run shared/__tests__/parse-command.spec.ts — all 67 tests pass.

How reviewers can verify:

  1. Run the heredoc test suite: cd src && npx vitest run shared/__tests__/parse-command.spec.ts
  2. Manually confirm the following inputs are now parsed as single valid commands (no parse error):
    sh -c bash << EOF; echo done
    echo hello
    EOF
    
    sh -c bash << EOF | grep hi
    echo hello
    EOF
    
    sh -c bash << EOF && cat f
    echo hello
    EOF
    

Pre-Submission Checklist

  • Issue Linked: This PR is linked to an approved GitHub Issue (see "Related GitHub Issue" above).
  • Scope: My changes are focused on the linked issue (one major feature/fix per PR).
  • Self-Review: I have performed a thorough self-review of my code.
  • Testing: New and/or updated tests have been added to cover my changes (if applicable).
  • Visual Snapshot (UI changes only): N/A — this is a backend parsing change, no UI.
  • Documentation Impact: No documentation updates are required.
  • Contribution Guidelines: I have read and agree to the Contributor Guidelines.

Visual Snapshots

N/A

Videos (interaction / animation only)

N/A

Documentation Updates

  • No documentation updates are required.

Additional Notes

N/A

Get in Touch

hehegwk_23849

Summary by CodeRabbit

  • Bug Fixes
    • Fixed heredoc parsing when command operators such as ;, |, and && appear on the opener line.
    • Prevented trailing operators, redirections, and parentheses from being incorrectly included in heredoc delimiters.
    • Improved handling of Windows-style line endings in heredocs.
    • Complete heredoc commands now parse correctly without errors.

A heredoc delimiter is a single shell word, so a control operator
(;, |, &&) on the opener line terminates the word and must not be
absorbed into the delimiter. Previously the parser kept scanning past
these operators, so the terminator line was never found and a valid
command was incorrectly rejected as an unterminated heredoc.

Add isHeredocDelimiterTerminator() to stop the delimiter at POSIX
control operators, matching shell tokenization, and apply it to both
the backslash-escaped and unquoted delimiter branches. Add regression
tests covering ;, |, and && on the opener line.

Signed-off-by: daewoongoh <dw.oh@samsung.com>
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bfb69c9b-1d9e-47da-897f-47520b66c103

📥 Commits

Reviewing files that changed from the base of the PR and between 36e406d and 2ad1546.

📒 Files selected for processing (2)
  • src/shared/__tests__/parse-command.spec.ts
  • src/shared/parse-command.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/shared/tests/parse-command.spec.ts
  • src/shared/parse-command.ts

📝 Walkthrough

Walkthrough

The heredoc parser now stops delimiter parsing at whitespace, line breaks, POSIX control operators, and carriage returns. Regression tests cover ;, |, &&, and CRLF heredocs.

Changes

Heredoc parsing

Layer / File(s) Summary
Delimiter boundary handling
src/shared/parse-command.ts, src/shared/__tests__/parse-command.spec.ts
The parser applies shared terminator rules to unquoted and backslash-escaped delimiters. Tests verify valid single-command heredocs with control operators and CRLF terminators.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • Zoo-Code-Org/Zoo-Code#483: Both changes update heredoc parsing, with this change adding control-operator and CRLF delimiter termination.

Suggested reviewers: edelauna

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary heredoc delimiter parsing fix.
Description check ✅ Passed The description includes the linked issue, implementation details, test procedure, checklist, and documentation impact.
Linked Issues check ✅ Passed The changes satisfy issue #1116 by stopping delimiters at control operators and adding regression coverage for valid heredocs.
Out of Scope Changes check ✅ Passed The CRLF handling and tests are directly related to heredoc delimiter termination and do not introduce unrelated changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

src/shared/__tests__/parse-command.spec.ts

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.

src/shared/parse-command.ts

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/shared/__tests__/parse-command.spec.ts (1)

293-310: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add coverage for quoted delimiters.

The PR objective requires control characters to remain literal when the delimiter is quoted. The current inputs cover only unquoted EOF, so they do not protect the quoted branches from regression.

 			const inputs = [
+				"sh -c bash <<'EOF;'\necho hello\nEOF;",
+				"sh -c bash <<\"EOF|\"\necho hello\nEOF|",
 				"sh -c bash << EOF; echo done\necho hello\nEOF",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared/__tests__/parse-command.spec.ts` around lines 293 - 310, Add
quoted-heredoc delimiter cases to the test in parseCommand, covering the
existing control-operator variants with delimiters quoted in supported forms.
Assert each input has a null parseError and remains a single opaque command,
matching the current unquoted expectations.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/shared/parse-command.ts`:
- Around line 150-163: Update isHeredocDelimiterTerminator to treat "\r" as a
delimiter terminator alongside "\n", preserving CRLF parsing and preventing
carriage returns from being included in heredoc delimiters. Add a regression
test through parseCommand() that verifies a CRLF heredoc terminates
successfully.
- Around line 181-189: Update the heredoc delimiter parsing around
isHeredocDelimiterTerminator to recognize backslash escapes before checking
token boundaries. In both bare-word parser branches and the outer heredoc opener
scan, consume \X as part of the delimiter and apply quote removal appropriately,
so escaped semicolons do not terminate the first delimiter word. Preserve normal
delimiter termination for unescaped characters.

---

Nitpick comments:
In `@src/shared/__tests__/parse-command.spec.ts`:
- Around line 293-310: Add quoted-heredoc delimiter cases to the test in
parseCommand, covering the existing control-operator variants with delimiters
quoted in supported forms. Assert each input has a null parseError and remains a
single opaque command, matching the current unquoted expectations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 280724f7-415a-43ff-a5a2-6d0d1e7e10ac

📥 Commits

Reviewing files that changed from the base of the PR and between 7918f6b and 36e406d.

📒 Files selected for processing (2)
  • src/shared/__tests__/parse-command.spec.ts
  • src/shared/parse-command.ts

Comment thread src/shared/parse-command.ts
Comment on lines 181 to 189
} else if (command[i] === "\\") {
i++ // skip backslash
while (i < command.length && command[i] !== "\n" && command[i] !== " " && command[i] !== "\t") {
while (i < command.length && !isHeredocDelimiterTerminator(command[i])) {
delimiter += command[i++]
}
} else {
while (i < command.length && command[i] !== "\n" && command[i] !== " " && command[i] !== "\t") {
while (i < command.length && !isHeredocDelimiterTerminator(command[i])) {
delimiter += command[i++]
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file outline =="
ast-grep outline src/shared/parse-command.ts --view expanded || true

echo "== relevant lines =="
nl -ba src/shared/parse-command.ts | sed -n '1,260p'

echo "== usages/tests =="
rg -n "parseCommand|heredoc|EOF|\\\\;|delimiter" -S src || true

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 1545


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== parse-command relevant lines using sed =="
sed -n '30,245p' src/shared/parse-command.ts

echo "== read-only behavioral probe =="
python3 - <<'PY'
from pathlib import Path
import ast

source = Path("src/shared/parse-command.ts").read_text()
# Identify the JS parser-like constants/functions that the file uses.
print("source includes isHeredocDelimiterTerminator:", "isHeredocDelimiterTerminator" in source)
print("source includes parseHeredocDelimiter:", "parseHeredocDelimiter" in source)

# Extract and evaluate the exact isHeredocDelimiterTerminator function from source so
# the probe uses the same predicate as the file.
start = source.index("function isHeredocDelimiterTerminator")
end = source.index("function parseHeredocDelimiter", start)
func = source[start:end]
print("== extracted isHeredocDelimiterTerminator ==")
print(func)

# Model the parser-controlled branch structure around the heredoc delimiter scan in parseHeredocDelimiter.
# This does not compile/execute repository code.
def is_delimiter_term(char):
    return char in " \t\r\n;"

def current_scan(command, start):
    i = start + 1
    delimiter = ""
    while i < len(command):
        if command[i] == "\\" and i + 1 < len(command):
            i += 2
            continue
        if not is_delimiter_term(command[i]):
            j = i
            while j < len(command) and not is_delimiter_term(command[j]):
                j += 1
            return command[start + 1:j], j
        break
    return "", i

# Case from the review: escaped semicolon outside quotes should produce delimiter "EOF;" with current behavior.
print("current_scan result for 'cat <<EOF\\\\;' command:", current_scan("cat <<EOF\\;", 4))
PY

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 9317


🌐 Web query:

POSIX shell heredoc delimiter word quote removal backslash escapes control operator

💡 Result:

In POSIX shell, the handling of heredocs (here-documents) depends on whether the heredoc delimiter word is quoted [1][2]. Quote Removal and Delimiters If any part of the delimiter word is quoted, the shell performs quote removal on the word to determine the final delimiter [1][3]. Crucially, if the word is quoted, the lines within the heredoc are not expanded (meaning no parameter expansion, command substitution, or arithmetic expansion is performed) [1][2]. The shell treats the heredoc content effectively as a literal, single-quoted string [4]. Backslash Escapes The behavior of backslashes inside a heredoc differs based on the quoting of the delimiter: 1. Unquoted Delimiter: If the delimiter word is unquoted, the shell performs expansions (parameter, command, and arithmetic) on the heredoc content [2]. Within this content, the backslash character is used to escape the special characters '$', '`', and '' [2]. Additionally, a backslash followed immediately by a newline is interpreted as line continuation and is removed [1][4]. 2. Quoted Delimiter: If the delimiter word is quoted, the heredoc content is not expanded [1][4]. Because the content is treated as a literal string, backslashes lose their special status as escape characters and are preserved as literal characters, including backslash-newline sequences [4]. Control Operators Heredocs are a form of redirection and are associated with a command [5]. They are independent of control operators like '|', ';', '&', '&&', or '||' [5]. A heredoc remains attached to the command it follows, regardless of whether that command is part of a pipeline or followed by a control operator [6]. The shell identifies the heredoc content by reading from the current input source until it encounters a line containing only the delimiter word (after any necessary quote removal) and a newline character [6][2]. If the operator '<<-' is used, the shell also strips leading tab characters from the heredoc lines and the line containing the delimiter [1][2]. Top Results: [1] https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/utilities/V3_chap02.html [4] https://unix.stackexchange.com/questions/340923/what-does-posix-require-for-quoted-here-documents-inside-command-substitution [5] https://pubs.opengroup.org/onlinepubs/9699919799.2008edition/utilities/V3_chap02.html [2] https://stackoverflow.com/questions/75535047/in-heredoc-what-is-the-delimiter-for-a-single-quote

Citations:


🌐 Web query:

site:pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html heredoc delimiter word word removal "escape"

💡 Result:

In shell scripting, the behavior of a heredoc delimiter regarding quoting and expansion is defined by the POSIX standard [1]. When you define a heredoc using a redirection operator like <<WORD, the shell treats WORD as the delimiter. If any part of this delimiter word is quoted (for example, <<'EOF', <<"EOF", or <<\EOF), the shell performs quote removal on the word to determine the final delimiter [1]. Crucially, if the delimiter is quoted in this way, the contents of the heredoc are treated as a literal string and are not subject to parameter expansion, command substitution, or arithmetic expansion [1]. Conversely, if the delimiter word is unquoted, the delimiter is the word itself, and the contents of the heredoc are fully expanded by the shell (performing parameter expansion, command substitution, and arithmetic expansion) after the trailing delimiter is located [1]. Additionally, if the redirection operator is <<- (rather than <<), the shell strips leading tab characters from each line of the heredoc as it is read, which is commonly used to allow for proper indentation of heredocs within code blocks while maintaining the intended formatting of the resulting document [1].

Citations:


🌐 Web query:

"2.7.2.1.4. Redirections" heredoc "delimiter word" quote removal POSIX

💡 Result:

According to the POSIX Shell Command Language specification (specifically section 2.7.4, Here-Document), the handling of the heredoc delimiter word and the subsequent expansion of the heredoc body are determined by whether the word is quoted [1][2][3]. If any part of the heredoc delimiter word is quoted, the following rules apply: 1. The delimiter used to terminate the heredoc is formed by performing quote removal on the original word [1][2][3]. 2. The lines within the heredoc body are not subject to any expansions (such as parameter expansion, command substitution, or arithmetic expansion) [1][2][3]. If no part of the heredoc delimiter word is quoted: 1. The delimiter is the word itself [1][2][3]. 2. All lines of the heredoc are expanded for parameter expansion, command substitution, and arithmetic expansion [1][2][3]. In this mode, the backslash character behaves as it does inside double-quotes, meaning it can be used to escape specific characters like the backslash itself, the dollar sign, and backticks [1][4]. Quote removal itself is defined in section 2.6.7 of the POSIX specification, which mandates that the quote characters (backslash, single-quote, and double-quote) present in the original word are removed unless they have themselves been quoted [1][2]. Quote removal is always the final step in the word expansion process [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== scanTopLevelQuotes heredoc branch =="
sed -n '215,295p' src/shared/parse-command.ts

echo "== parseCommand parseHeredocDelimiter call sites =="
rg -n "parseHeredocDelimiter|heredoc|delimiter" src/shared/parse-command.ts

echo "== behavioral comparison for unquoted delimiter with escaped control operator =="
python3 - <<'PY'
def is_delimiter_term(char: str) -> bool:
    return char in "\n \t;|&><()"

def current_parse_heredoc_delimiter(command: str, start: int):
    i = start
    delimiter = ""
    if command[i] == "'":
        i += 1
        while i < len(command) and command[i] != "'" and command[i] != "\n":
            delimiter += command[i]
            i += 1
        if i < len(command) and command[i] == "'":
            i += 1
    elif command[i] == '"':
        i += 1
        while i < len(command) and command[i] != '"' and command[i] != "\n":
            delimiter += command[i]
            i += 1
        if i < len(command) and command[i] == '"':
            i += 1
    elif command[i] == "\\":
        i += 1
        while i < len(command) and not is_delimiter_term(command[i]):
            delimiter += command[i]
            i += 1
    else:
        while i < len(command) and not is_delimiter_term(command[i]):
            delimiter += command[i]
            i += 1
    return delimiter, i

def fixed_unquoted_parse_heredoc_delimiter(command: str, start: int):
    i = start
    delimiter = ""
    while i < len(command):
        if command[i] == "\\":
            if i + 1 < len(command):
                delimiter += command[i + 1]
                i += 2
            else:
                # Trailing unquoted escape: treat backslash literally, then stop.
                delimiter += "\\"
                i += 1
            continue
        if is_delimiter_term(command[i]):
            break
        delimiter += command[i]
        i += 1
    return delimiter, i

for command in ["cat <<\\;", "cat <<EOF\\;", "cat <<EOF\\n", "cat <<EOF\\|"]:
    print(f"{command!r}: current={current_parse_heredoc_delimiter(command, 4)}, fixed={fixed_unquoted_parse_heredoc_delimiter(command, 4)}")
PY

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 5706


🌐 Web query:

POSIX quote-removal double quote backslash follows ordinary character preserved escaped

💡 Result:

According to the POSIX Shell Command Language standard, the behavior of a backslash within double quotes is as follows: 1. Special Meaning: Inside double quotes, a backslash retains its special meaning as an escape character only when it is immediately followed by one of the following "special" characters: $, `, ", , or a [1][2][3]. 2. Processing: When a backslash is followed by one of these special characters, the backslash is deleted (removed), and the subsequent character is preserved literally [1][4][3]. 3. Ordinary Characters: If a backslash is followed by a character that is not one of the special characters listed above, both the backslash and the character following it are left unmodified [4][3]. The backslash is not removed in this case [5]. For example, the string "\a" inside double quotes remains "\a" because 'a' is not a special character that a backslash escapes within double quotes [3]. Conversely, "\" results in a single literal backslash because the first backslash escapes the second, causing the first to be removed during quote removal [3]. Quote removal itself is a distinct phase performed last in the shell's processing of a word [1][2]. During this phase, the quote characters (including double quotes and backslashes used for escaping special characters) that were present in the original word are removed, unless they were themselves quoted [6][1][7]. Top Results: [6][1][4][3]

Citations:


🌐 Web query:

POSIX 2.7.1.1 "If any part of the word is quoted" here-document "quote-removal" "backslash"

💡 Result:

According to the POSIX Shell Command Language specification (specifically section 2.7.4, Here-Document), the handling of a here-document depends on whether the delimiter word is quoted [1][2][3]. If any part of the delimiter word is quoted, the shell performs quote removal on the word to determine the final delimiter [1][2]. Crucially, when the delimiter is quoted, the lines of the here-document are not expanded; they are treated literally [1][2][4]. Because the here-document content is not expanded, the backslash character does not act as an escape character (and backslash-newline line continuation is not performed) within the here-document body [5]. In contrast, if no part of the delimiter word is quoted, the shell does not perform quote removal on the delimiter, and the content of the here-document is expanded for parameter expansion, command substitution, and arithmetic expansion [1][2][6]. In this unquoted mode, the backslash character inside the here-document behaves similarly to how it behaves inside double-quotes, meaning it may be used to escape specific characters [1][2][6]. The phrase "If any part of the word is quoted" refers to the presence of quoting characters (like backslashes, single-quotes, or double-quotes) in the delimiter word provided by the user [1][7]. When these are present, they are removed to form the actual delimiter, and the resulting here-document body remains entirely uninterpreted [1][2][5].

Citations:


🌐 Web query:

POSIX 2.7.1.4 "here-document" <<\ "\\" \\ quote removal

💡 Result:

Under the POSIX Shell Command Language specification, particularly section 2.7.4 (Here-Document), the use of a quoted delimiter—such as in the form <<'word' or <<\word—activates a specific behavior for the here-document [1][2]. When any part of the delimiter word is quoted, the shell performs the following steps: 1. Quote Removal: The shell performs quote removal on the delimiter word to determine the actual string that will serve as the terminating delimiter [1][3]. 2. Inhibition of Expansion: Because the delimiter is quoted, the here-document lines are not subject to parameter expansion, command substitution, or arithmetic expansion [1][2][3]. 3. Literal Interpretation: The contents of the here-document are treated as literal text, effectively as if the entire block were single-quoted [4]. This means characters like backslashes are not treated as escape characters, and the special handling of backslash-newline for line continuation does not occur within the body of the here-document [4][5]. The delimiter itself is identified by taking the word provided in the redirection operator and applying quote removal to it [1][2]. If the input word was <<\word, the backslash is removed during the quote removal phase, resulting in the delimiter "word" [1][5]. Consequently, the shell will continue to read lines from the input until it encounters a line that consists solely of "word" (after any necessary stripping if the <<- operator is used) [1][2][3]. In summary, quoting the delimiter ensures that the here-document remains entirely uninterpreted, preserving the input exactly as written [5][6].

Citations:


🌐 Web query:

"2.7.1.4. Here Documents" "If any part of the word is quoted" quote removal "\ "

💡 Result:

In the context of the POSIX Shell Command Language, the rule regarding quoted here-document delimiters is found within the specifications for Here-Documents [1][2]. If any part of the word (the delimiter) is quoted, the following rules apply: 1. Quote removal is performed on the word to determine the actual delimiter string [1][2]. 2. The lines of the here-document are not expanded; parameter expansion, command substitution, and arithmetic expansion are inhibited [1][3]. 3. The content of the here-document is treated effectively as if it were inside single quotes [4]. If no part of the word is quoted, the delimiter is the word itself (without modification), and the contents of the here-document are subject to standard expansion (parameter expansion, command substitution, and arithmetic expansion), behaving similar to text within double quotes (with specific exceptions for the double-quote character itself) [2][5]. The phrase "If any part of the word is quoted" refers to the shell's mechanism for handling quoting characters (backslash, single-quote, or double-quote) present in the delimiter word [1][2]. Quote removal, which occurs last, is the process of removing these special quote characters from the word to determine the final, literal string used to terminate the here-document [2][5].

Citations:


Skip backslash escapes before delimiter-word tokenization.

cat <<\; and cat <<EOF\; are quoted heredoc delimiters, so the backslash participates in quote removal rather than ending the first delimiter word. Handle \X inside both the bare-word parser paths and the outer heredoc opener scan before isHeredocDelimiterTerminator() can treat the escaped character as a token boundary.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared/parse-command.ts` around lines 181 - 189, Update the heredoc
delimiter parsing around isHeredocDelimiterTerminator to recognize backslash
escapes before checking token boundaries. In both bare-word parser branches and
the outer heredoc opener scan, consume \X as part of the delimiter and apply
quote removal appropriately, so escaped semicolons do not terminate the first
delimiter word. Preserve normal delimiter termination for unescaped characters.

@github-actions github-actions Bot added the awaiting-review PR changes are ready and waiting for maintainer re-review label Aug 4, 2026
The heredoc body scanner already strips CR before comparing the
terminator line, but the delimiter parser did not treat CR as a word
terminator. On CRLF input the delimiter became e.g. `EOF\r`, which never
matched the `EOF` terminator and caused a valid heredoc to be rejected
as unterminated.

Add CR to isHeredocDelimiterTerminator() so the delimiter ends at CR on
CRLF line endings, and add a regression test covering a CRLF heredoc.

Signed-off-by: daewoongoh <dw.oh@samsung.com>
@daewoongoh daewoongoh closed this Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting-review PR changes are ready and waiting for maintainer re-review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Heredoc delimiter absorbs trailing control operator (;, |, &&) causing valid commands to be rejected as unterminated

1 participant