管道数据被 heredoc 静默吞掉:python3 - 与 <<'PY' 不能共存
一个 systemd 定时任务每分钟读取日志增量、用内嵌 python 解析成 NDJSON 后上报。日志里明明有记录,监控台永远显示「本轮 delta 无可解析条目」。把 python 代码单独抽成 .py 文件,用相同数据管道进去——解析完全正常。脚本本身是唯一的嫌疑人。
现象
脚本大致结构如下:
TAIL=$(tail -c "+$((OFFSET + 1))" "$LOG_FILE")
NDJSON=$(printf '%s' "$TAIL" | python3 - "$INSTANCE_ID" "$ENV" <<'PY'
import sys
raw = sys.stdin.read() # 预期:拿到 TAIL 的内容
blocks = re.split(r'(?=^# Time: )', raw, flags=re.M)
# 解析 0 条,因为 raw 是 ""
PY
)现象清单:
$TAIL有 385 字节,$NDJSON为空- python 不报错,
$?为 0,set -e不触发 - 往 python 里加
print(f"raw_len={len(raw)}", file=sys.stderr)→ 输出raw_len=0 - 把同一段 python 写进独立
parser.py,printf '%s' "$TAIL" | python3 parser.py→ 正常解析
根因
python3 - 的 - 是「从 stdin 读取程序源码」的信号。这条命令上同时存在两个 stdin 重定向:
- 管道(
printf ... | python3):想把TAIL送进 python 的 stdin。 - heredoc(
<<'PY' ... PY):同样接管 python 的 stdin,把脚本源码送进去。
bash 在命令解析阶段就把 heredoc 绑定为 stdin;管道的另一端写入的数据,根本没有文件描述符去接收,静默丢弃。python 从 stdin 读到了自己的源代码(heredoc 内容),执行完毕,sys.stdin.read() 返回空字符串,一切正常退出。
cmd arg <<'DELIM' 与 ... | cmd arg 一旦写在同一行,heredoc 的重定向优先级高于管道。python/perl/awk 等解释器使用 - 旗标表示「程序源码来自 stdin」时,就会触发这个陷阱。
最险的地方在于:没有任何报错。python 确实读到了「合法的 python 源码」(heredoc 内容),执行后输出为空,而下游把「空输出」当成「没有新数据」——这条静默的失败路径很可能是业务的正常分支,永远不会报警。
正解
把 heredoc 换成 bash 变量持有脚本源码,再用 python3 -c "$SCRIPT" 执行。这样 stdin 没有被 heredoc 占用,管道数据可以正常到达 sys.stdin.read()。
# 用 read 把源码吸进变量;-d '' 表示读到 EOF 为止
# || true 是必要的:read 在 EOF 返回 1,set -e 会误杀脚本
read -r -d '' PYSCRIPT <<'PY' || true
import sys, json, re
raw = sys.stdin.read() # 现在真的能收到管道数据了
blocks = re.split(r'(?=^# Time: )', raw, flags=re.M)
# ... 正常解析 ...
PY
# heredoc 已被 read 消耗;python3 -c 从参数拿脚本,stdin 留给管道
NDJSON=$(printf '%s' "$TAIL" | python3 -c "$PYSCRIPT" "$INSTANCE_ID" "$ENV")两个细节不能省:
<<'PY'(带引号的定界符):heredoc 内容原样保留,bash 不展开$VAR——嵌入 python/perl 时几乎总想要这个行为。|| true:read -d ''到达 EOF 时以退出码 1 结束;若脚本有set -e,不加|| true会在 python 运行前就中止。
同样的修法适用于所有「- 表示从 stdin 读程序」的解释器:
# perl
read -r -d '' PERLSCRIPT <<'PL' || true
while (<STDIN>) { print uc($_) }
PL
printf '%s' "$DATA" | perl -e "$PERLSCRIPT"把 heredoc 从命令重定向改为 read 赋值,stdin 就解放给了管道。
快速确认是否中招
加一行 stderr 探针,不改逻辑:
NDJSON=$(printf '%s' "$TAIL" | python3 - "$ARG" <<'PY'
import sys
print(f"raw_len={len(sys.stdin.read())}", file=sys.stderr)
PY
)- 输出
raw_len=0而$TAIL非空 → 确认命中此坑 - 输出
raw_len>0→ 是解析逻辑的问题,不是这个坑
一句话外卖
python3 - <<'PY'里的-和<<都要 stdin,heredoc 必胜;要嵌入解释器脚本同时读管道,用read -r -d '' VAR <<'PY' || true先把源码存进变量,再用-c "$VAR"执行。