CRLF 与 LF 破坏 CI 的换行符问题
您的shell脚本在本地可以正常运行,但在CI环境中会报错'bad interpreter: /bin/bash^M'。这是CRLF换行符的问题。了解其成因、如何检测以及如何通过.gitattributes永久修复。
你的 shell 脚本在笔记本电脑上运行完美。你将其推送到 GitHub,但 CI 流程却出现一个神秘的错误。 /bin/bash^M: bad interpreter 你刚刚遭遇了软件开发中最隐蔽的 bug:错误的行结束符。
本文将解释 CRLF 和 LF 实际上是什么,为什么混合使用会静默地破坏脚本和配置文件,以及如何采取确切步骤,防止行结束符错误再次进入你的流水线。
CRLF 和 LF 是什么?
每个文本文件都需要一种方式来标记行的结束。两种约定源自物理电传打字机时代:
- LF(换行符) — 一个
\n字符(字节0x0A)。在 Linux、macOS 和所有基于 Unix 的系统中使用。 - CRLF(回车符 + 换行符) — 两个字符,
\r\n(字节0x0D 0x0A)。在 Windows 和 MS-DOS 中使用。
这些名称源自打字机的工作原理。一个 回车符 将打印头移动到行的开头。一个 换行符 将纸张推进一行。Windows 保留了两者;Unix 则去除了冗余的回车符。
为什么 CRLF 会破坏 CI 流水线?
在 Linux 上(几乎所有 CI 运行器都在此环境执行), \r 不是空白字符——它是一个字面字符。当 shell 脚本以 CRLF 结尾保存时,每行末尾都会包含 \r 在换行符之前。内核将 shebang 行解释为 #!/bin/bash\r 并寻找一个名为 bash\r的二进制文件。该二进制文件并不存在。
产生的错误看起来如下:
: bad interpreter: /bin/bash^M: No such file or directory
这 ^M 是终端显示回车字符的方式。它在大多数文本编辑器中是不可见的,这使得这个 bug 非常令人困惑。
CRLF 在其他地方静默造成破坏
- Dockerfile — 包含 CRLF 的
RUN指令会在每个命令中注入\r,从而破坏字符串比较和文件路径。 - Python 脚本 —
SyntaxError: unexpected character after line continuation character当\后跟\r\n. - .env 文件 — 环境变量值会附加一个尾随的
\r,因此APP_ENV=production\r永远不会匹配预期的production. - CSV 和数据文件 — 每行逐行解析的解析器可能会在每一行的最后一个字段中包含
\r。 - SSH authorized_keys — 使用 CRLF 编码的密钥文件将被 SSH 服务端静默拒绝。
- Git 差异 — 每一行都显示为已更改,掩盖了真正的变更。
如何检测行结束符问题?
大多数编辑器默认隐藏行结束符。以下是可靠的检测方法:
使用 cat 或 hexdump
# Show ^M characters
cat -A yourfile.sh | head -5
# Hex dump to see 0x0d (CR) characters
hexdump -C yourfile.sh | head -10
使用 file 命令
file yourfile.sh
# CRLF output: yourfile.sh: ASCII text, with CRLF line terminators
# LF output: yourfile.sh: ASCII text
使用 grep
# Returns exit code 0 (found) if CRLF endings exist
grep -rlP "\r" . --include="*.sh" --include="*.py" --include="*.yml"
如何修复 CRLF 行结束符?
选项 1:dos2unix(最快的一次性修复)
dos2unix 移除文件中的回车符。它在所有主要 Linux 发行版中都可用:
# Fix a single file
dos2unix yourscript.sh
# Fix all shell scripts recursively
find . -name "*.sh" -exec dos2unix {} \;
# Reverse: convert LF to CRLF (unix2dos)
unix2dos yourfile.sh
选项 2:sed(无需额外工具)
# Remove carriage returns in-place
sed -i 's/\r//' yourscript.sh
# Or using tr
tr -d '\r' < input.sh > output.sh
选项 3:在 VS Code 或 JetBrains 中修复
在 VS Code 中,行结束符模式显示在状态栏(右下角)。点击它以切换当前文件的 CRLF 和 LF。要更改新文件的默认设置,请在 "files.eol": "\n" 中设置 settings.json.
在 JetBrains IDE 中,前往 文件 → 行分隔符 以更改当前文件,或在 编辑器 → 代码样式 → 行分隔符.
中设置默认值。
正确的解决方案:.gitattributes 文件 .gitattributes 一次性修复无法扩展。正确的解决方案是创建一个
# .gitattributes — commit this to the root of your repository
# Default: normalize all text files to LF in the repo
* text=auto eol=lf
# Explicitly enforce LF for scripts and configs
*.sh text eol=lf
*.bash text eol=lf
*.py text eol=lf
*.rb text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.json text eol=lf
*.env text eol=lf
Dockerfile text eol=lf
Makefile text eol=lf
# Windows-only files can keep CRLF
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
# Binary files — never touch line endings
*.png binary
*.jpg binary
*.gif binary
*.zip binary
*.pdf binary
文件,该文件明确告诉 Git 应该强制使用哪些行结束符,无论贡献者使用的是哪种编辑器或操作系统。
git add --renormalize .
git commit -m "chore: normalize line endings via .gitattributes"
添加此文件后,运行以下命令以一次性重新规范化整个仓库:
Git 的 autocrlf 设置——以及为何它常常使问题更严重 core.autocrlf Git 有一个
core.autocrlf=true设置,旨在自动转换行结束符:core.autocrlf=input— 在检出时将 LF 转换为 CRLF(Windows),在提交时将 CRLF 转换为 LF。适用于 Windows 用户。core.autocrlf=false— 在提交时将 CRLF 转换为 LF,在检出时不做任何操作。更安全用于 Mac/Linux。
— Git 不做任何操作。无论编辑器保存的内容是什么,都会被提交。 core.autocrlf 问题: 是存储在 中的一个本地设置。团队中的每个开发人员都有不同的值,因此来自不同机器的提交会产生不同的行结束符。这在差异中产生持续噪音,并在不同开发者最后修改文件时导致间歇性 CI 失败。 ~/.gitconfig。团队中的每个开发人员都有不同的价值,因此来自不同机器的提交会产生不同的行末符。这会在差异对比中产生持续的噪音,并导致CI构建失败,具体取决于最后修改文件的是哪位开发人员。
经验法则: 使用 .gitattributes 在仓库中设置行结束符策略。让 core.autocrlf 保持为每个开发人员的原始值—— .gitattributes 会覆盖它。
添加 CI 防护
即使设置了 .gitattributes ,仍建议在 CI 中添加显式的检查,以捕获任何可能漏掉的文件。一个两行步骤即可覆盖大多数情况:
# In your CI workflow (GitHub Actions example)
- name: Check for CRLF line endings
run: |
if grep -rlP "\r" . --include="*.sh" --include="*.py" --include="*.yml" --include="Dockerfile"; then
echo "ERROR: CRLF line endings found. Run dos2unix on the above files."
exit 1
fi
此步骤在源头(PR)处明确失败,而不是在部署时静默失败。
快速参考:CRLF 与 LF
LF(\n) | CRLF(\r\n) | |
|---|---|---|
| 字节 | 0x0A | 0x0D 0x0A |
| 使用于 | Linux、macOS、Unix | Windows、MS-DOS |
| 适用于 shell 脚本 | 是的 | 不适用——破坏 shebang |
| 适用于 Dockerfile | 是的 | 不 |
| 适用于 .env 文件 | 是的 | 不适用——在值中添加尾随 \r |
| Git 推荐 | 在仓库中规范化为 LF | 仅适用于 .bat/.cmd/.ps1 文件 |
