---
date: 2026-05-18
type: error
tags: [bash, heredoc, shell-quoting, python-embedding, ci-script, gitea-api]
---

# Bash heredoc 里嵌 `<` 字符（如 `<remember>` / XML / JSX）触发 shell 重定向解析

## 现象

写 PR body / 给外部 API 传 JSON 时，习惯用：

```bash
python3 - <<PY
body = '''
... <remember>tag</remember> ...
'''
PY
```

bash 报 `syntax error near unexpected token 'newline'` 在 `<remember>` 那行。
heredoc 终止符前的内容里只要含**未被引号包住的** `<` 紧跟空白 / 换行，bash
**会先做参数扩展** 把 `<remember>` 当成"从文件 `remember` 重定向"，看到下一个 `>`
当成输出重定向，整个 substitution 状态机错乱。

## 根因

bash heredoc 的 **delimiter 是否被引号包裹** 决定行内 `<` `>` `$` 怎么处理：

| heredoc 形式 | 行内 `<` / `>` |
|---|---|
| `<<PY` (delimiter 裸的) | 仍做参数扩展 + 命令替换；`<word>` 当 redirection 解析 |
| `<<'PY'` (delimiter 加单引号) | **完全字面**，`<remember>` 当字符串 |

我用的 `<<PY`（裸 delimiter）+ Python 三引号字符串里塞 `<remember>` → bash 先解析。
更糟的是当 heredoc 通过 `$(...)` 命令替换被双层包裹时，bash 状态机对 `<>` 的判定更
脆弱，常见症状是 `syntax error near unexpected token 'newline'`，错误行号还指向
heredoc 中部，根本看不出是 `<` 的锅。

## 解法

**复杂 markdown / JSON / XML body 永远走"写文件 + 引用"模式**，绝不内联 heredoc：

```bash
# Step 1: 把 body 写到临时文件（用 Write 工具或 cat <<'EOF' > file 配 quoted delimiter）
cat <<'EOF' > /tmp/pr-body.md
... <remember>...</remember> ... `$variables` ...
EOF

# Step 2: 用 Python 读文件构造请求
GTOKEN=$TOKEN python3 - <<'PY'
body = open("/tmp/pr-body.md").read()
# build request from body...
PY
```

关键：
1. **heredoc delimiter 一律加单引号** (`<<'PY'`, `<<'EOF'`) —— 全字面，不解析
2. **大段含特殊字符的内容写文件，命令里只读文件**
3. **env var 跨进程**：bash → python 之间传 token 用 `VAR=value python3 ...` 写到 env，不要内嵌 `"$TOKEN"` 字符串拼接（避免 token 含特殊字符再次解析）

## 适用范围

- 创建 PR / issue（body 含 markdown / code block / `<tag>` 示例）
- 发 webhook / API JSON payload 含 XML / JSX 字符串字面量
- CI 脚本里给外部服务推 release notes
- 任何"复杂结构内容 + 跨工具传递"

## 关联

- CLAUDE.md「Gitea 平台配置」段：「JSON body 复杂时用 Python 生成，避免 shell 转义坑」
  —— 跟本条同一坑的简短版（已存在但只说"用 Python"，没说"且要用 quoted delimiter + 文件中转"）
- [ERR-20260515-003](ERR-20260515-003-shell-embedded-python-cn-quotes.md): shell 嵌入 Python 中文引号陷阱，相关但不同（中文字符 vs `<` 字符）

## 教训

1. **`<<PY` 跟 `<<'PY'` 不是一回事**：单引号包 delimiter 才完全字面化
2. **bash 错误行号会指 heredoc 中部，掩盖根因在 `<`**：诊断时把 body 改成最简单"hello world" 试一下，能跑就锁定是 body 内容
3. **写大段复杂内容用文件中转是黄金标准**：Write 工具 → /tmp/xxx → python 读 → POST；省心
