BOM 是 Windows 侧写进去的,报错在 Linux 端炸开:PowerShell 默认编码、compose 与服务端 SQL 的三处现场
同一份「带 BOM 的 UTF-8」,在 Windows 上一切正常,落到 Linux 的配置解析器、容器编排器或 MySQL 客户端时,BOM 会变成内容的一部分。这里讨论的是写入端默认值和「配置文件被当作数据读」这条链路;CSV 导出侧、以及 shell/Go/JSON 解析器的报错对照不在范围内。
PowerShell 的默认值:5.1 与 7 不是一套规则
Windows PowerShell 里,除 UTF7 外的任何 Unicode 编码「总是」创建 BOM;PowerShell v6+ 的所有文本输出默认 utf8NoBOM。官方文档 about_Character_Encoding 与 understanding-file-encoding 分别给了枚举行为和跨平台注意事项。
| 输出方式 | Windows PowerShell 5.1 | PowerShell 7 |
|---|---|---|
Set-Content / Add-Content(不带 -Encoding) | Default(系统 ANSI 代码页),目标文件为空或不存在时生效 | utf8NoBOM |
Out-File / > / >> | UTF-16LE | utf8NoBOM |
Export-Csv | Ascii | utf8NoBOM |
Export-Clixml、New-ModuleManifest | UTF-16LE | utf8NoBOM |
[IO.File]::WriteAllText | 无 BOM | 无 BOM |
New-Item -Type File -Value | 无 BOM 的 UTF-8 | 无 BOM 的 UTF-8 |
5.1 的 -Encoding 取值是 Ascii / BigEndianUnicode / BigEndianUTF32 / Byte / Default / Oem / String / Unicode / Unknown / UTF32 / UTF7 / UTF8,其中 UTF8 的含义就是「带 BOM」。7.1+ 才拆成 utf8(无 BOM)、utf8BOM、utf8NoBOM,而 utf8NoBOM 在 5.1 上不是静默降级,是参数绑定直接报错。
另外几个容易踩的点:Out-File -Append 和 >> 不匹配已有文件的编码,直接用默认值追加;Add-Content 不带 -Encoding 时会探测已有编码。$PSDefaultParameterValues['Out-File:Encoding'] = 'utf8' 能改默认,但 5.1 下 utf8 仍然带 BOM。Send-MailMessage 默认 Ascii,Start-Transcript 在 5.1 下写带 BOM 的 UTF-8。
文档里还有一句反向的坑:Windows PowerShell 若脚本含非 ASCII 且文件本身无 BOM,会按旧 ANSI 代码页(en-US 下 Windows-1252)误解码;输入编码无法强制指定。
实测:同一行文本的五种写法
被测文本是 not ok 1,在 5.1 里分别写出并统计字节:
| 写法 | 字节数 | 开头字节 | BOM | /^not ok/ 是否匹配 |
|---|---|---|---|---|
Set-Content(默认) | 10 | 6e6f74206f6b2031 | 无 | 1 |
Add-Content(默认) | 10 | 同上 | 无 | 1 |
Out-File(默认) | 22 | fffe6e006f007400 | UTF-16LE | 0 |
> 重定向 | 22 | 同上 | UTF-16LE | 0 |
Out-File -Encoding utf8 | 13 | efbbbf6e6f74206f | UTF-8 BOM | 0 |
[IO.File]::WriteAllText | 8 | 6e6f74206f6b2031 | 无 | 1 |
注意最后两列的差别:-Encoding utf8 让解码对了,但 ^ 锚定失效——被匹配的是 BOM 那一侧。不带锚点的 grep 依然能命中,所以这类问题很容易在人工验证时蒙混过去。
5.1 上想写出无 BOM 的 UTF-8,可用 [System.IO.File]::WriteAllText($path, $text),或 $text | Set-Content run.log。读取端则要两步:解码正确不够,还得剥掉开头的 U+FEFF,例如 readFileSync(f, "utf8").replace(/^\uFEFF/, "")。
docker compose:env_file 里的键名多了个 BOM
compose#5220(2017,1.18.0 修复,PR #5303):config.env 以 UTF-8 BOM 开头,env_file 中的变量读不到。docker-compose config 展开后的结果是键名被污染:
environment:
NUGET_FALLBACK_PACKAGES: /root/.nuget/fallbackpackages
"\uFEFFTEST": Compose
同一份内容去掉 BOM 的 config2.env,展开为正常的 TEST: Compose。触发源是 Visual Studio 新建文本文件默认加 UTF-8 BOM。
compose 文件本身带 BOM:version 变成服务名
compose#6015 的原始报错:
ERROR: In file '.\docker-compose.yml', service 'version' must be a mapping not a string.
环境是 Windows Server 2016,1.15.0-rc1 正常,1.20.x / 1.21.2 失败,Linux 版 compose 不复现。维护者的分析是:BOM 在 CP1252 下也是合法字符,读文件时不抛异常,于是 EF BB BF 被解码成 ,粘在第一个键 version 前面,成了「服务名」。
服务端:BOM 被当成 SQL 数据送进 MySQL
phpMyAdmin issue #8362(2006,2.9.0 修复):导入 UTF-8 带 BOM 的 SQL 文件时 BOM 未被剥离,直接进入 MySQL,报 1064 语法错误。general query log 里的痕迹是:
3776 Query REPLACE socken1 VALUES (...)
MySQL Workbench 侧是 Bug #74914:6.2.3 起执行 UTF-8 编码的脚本报
Error Code: 1064 ... near 'CREATE TABLE IF NOT EXISTS profile(' at line 1
把文件存成「UTF-8 w/o BOM」即恢复正常,6.1.7 无此问题。后续跟帖(6.3.8、CRLF 换行)确认:带 EF BB BF 前导的文件失败;Ctrl-A 全选会把 BOM 一起选进去,所以手动执行仍然失败,从第一个可见字符开始向下拖选才能跑。结论落在 legacy MySQL Workbench 8.x 的实现上,该分支已停止维护,Workbench 26.7 重写后不再受影响。
检查与清洗
确认文件是否真带 BOM:
head -c 3 f | xxd
hexdump -C f | head -1
批量扫描:
grep -rlI $'^\xEF\xBB\xBF' .
git grep -I -l $'\xef\xbb\xbf'
Linux 侧就地清洗(LC_CTYPE=C 保证按字节匹配):
LC_CTYPE=C sed -i '1s/^\xEF\xBB\xBF//' file
.gitattributes 里的 *.sh text eol=lf 只做换行规范化,不会删 BOM,别指望它兜底。CI 里把上面那条 git grep 加进检查步骤,配合 PowerShell 侧统一用 utf8NoBOM(7)或 [IO.File]::WriteAllText(5.1),可以把这条链路从写入端收掉。