案例 BOM 是 Windows 写进去、在 Linux 炸开的:PowerShell 5.1 默认编码、compose 的 \uFEFFTEST 与 MySQL 1064

2026-10-22 00:04:33

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.1PowerShell 7
Set-Content / Add-Content(不带 -Encoding)Default(系统 ANSI 代码页),目标文件为空或不存在时生效utf8NoBOM
Out-File / > / >>UTF-16LEutf8NoBOM
Export-CsvAsciiutf8NoBOM
Export-Clixml、New-ModuleManifestUTF-16LEutf8NoBOM
[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(默认)106e6f74206f6b2031无1
Add-Content(默认)10同上无1
Out-File(默认)22fffe6e006f007400UTF-16LE0
> 重定向22同上UTF-16LE0
Out-File -Encoding utf813efbbbf6e6f74206fUTF-8 BOM0
[IO.File]::WriteAllText86e6f74206f6b2031无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),可以把这条链路从写入端收掉。

推荐文章

程序员茄子在线接单