用 PATH shim 从源头修复 Git Bash 调用 cmd.exe 的中文乱码
现象
在 zh-CN Windows 上的 Git Bash 中执行:
cmd /c "echo 中文测试"输出是 cmd 启动横幅加一行乱码,而且 echo 命令根本没执行:
Microsoft Windows [�汾 10.0.19045.6466]
(c) Microsoft Corporation����������Ȩ����
C:\Users\lhw>PowerShell 中调用同一条命令则正常。问题只出现在 Git Bash 启动 cmd.exe 时。
两个根因,不是一个
逐项隔离测试后,定位到两个互相独立的问题:
根因 1:MSYS 自动把 /c 转成 C:\
MSYS 在调用原生 Windows 可执行文件时,会把看起来像 POSIX 路径的参数自动转换成 Windows 路径。/c 被识别为 C: 盘根目录,转换成 C:\。
cmd.exe 实际收到的命令行是 cmd.exe C:\ "echo 中文测试",既没有 /c 标志,第一个参数也不是可识别的命令,cmd 直接进入交互模式,打印横幅,然后退出。这就是为什么 echo 永远不会执行。
验证方法 —— 用双斜杠绕过转换:
cmd //c "chcp & echo 中文测试"输出:
活动代码页: 936
���IJ���这次 /c 没被转换,cmd 正常执行了命令。但又遇到了根因 2。
根因 2:cmd.exe 管道输出使用 OEM 代码页
cmd.exe 输出到管道(不是控制台)时,会用系统 OEM 代码页编码字节。zh-CN 系统是 936/GBK。Git Bash 期望读到 UTF-8,所以 GBK 字节被当作 UTF-8 解析就成了乱码。
直觉的修复方式是在命令里加 chcp 65001:
cmd //c "chcp 65001 >nul && echo 中文测试"5 次重复运行的实际结果:
Run 1: ������� hello 1
Run 2: 你好世界 hello 2
Run 3: 你好世界 hello 3
Run 4: 你好世界 hello 4
Run 5: 你好世界 hello 5第一次仍然乱码。chcp 65001 对同一进程内紧接着的 echo 输出有竞态:代码页切换和管道输出缓冲的时机不一致,首次调用经常拿不到新代码页。这意味着 chcp 不是可靠的修复手段。
修复方案:PATH shim + iconv
在 PATH 里放一个名为 cmd 的 bash 脚本拦截 cmd.exe 调用,避免在每个调用处手动加补丁。
选型理由
Bash 工具实际运行的是 bash -c <command>,是非交互非登录 shell,不会加载 ~/.bashrc。所以函数、别名、~/.bashrc 配置都无效。能稳定生效的只有 PATH 拦截。
Git Bash 默认 PATH:
/c/Users/lhw/bin ← 第一位,用户可写
/mingw64/bin
/usr/local/bin
/usr/bin
/bin
...
/c/Windows/system32 ← cmd.exe 在这里在 C:\Users\lhw\bin\ 放一个名为 cmd(无 .exe 扩展名)的 bash 脚本,MSYS bash 解析命令时会优先命中它。Windows 本身只识别 .exe / .bat / .cmd,没有扩展名的 cmd 文件对 PowerShell、Run 对话框、其他 Windows 程序都不可见,所以拦截范围严格限定在 Git Bash 内部。
脚本内容
C:\Users\lhw\bin\cmd:
#!/usr/bin/env bash
export MSYS_NO_PATHCONV=1
export MSYS2_ARG_CONV_EXCL='*'
REAL_CMD="/c/Windows/System32/cmd.exe"
SOURCE_CP="${CMD_SHIM_SOURCE_CP:-CP936}"
run_command_mode() {
if [[ "${CMD_SHIM_PASSTHROUGH:-0}" == "1" ]]; then
exec "$REAL_CMD" "$@"
fi
"$REAL_CMD" "$@" \
2> >(iconv -f "$SOURCE_CP" -t UTF-8 -c >&2) \
| iconv -f "$SOURCE_CP" -t UTF-8 -c
exit "${PIPESTATUS[0]}"
}
if [[ -t 0 && -t 1 ]]; then
exec "$REAL_CMD" "$@"
else
run_command_mode "$@"
fi关键设计点
MSYS_NO_PATHCONV=1+MSYS2_ARG_CONV_EXCL='*':彻底禁用 MSYS 对参数的路径转换,/c、/k等开关原样传给 cmd.exeREAL_CMD使用绝对路径,避免脚本递归调用自己- 用
iconv -f CP936 -t UTF-8 -c转换 stdout 和 stderr,而不是依赖chcp 65001。iconv 是无状态的字节转换,没有竞态 -c让 iconv 静默丢弃源编码中无效的字节,避免遇到二进制数据时报错退出2> >(iconv ... >&2)使用 bash 进程替换分别转换两条流,stderr 不会被合并到 stdoutPIPESTATUS[0]保留 cmd.exe 的真实退出码,否则会被 iconv 的退出码覆盖- TTY 检测:stdin 和 stdout 都是终端时直接 passthrough,避免破坏交互式 cmd(虽然 MinTTY 下交互式 cmd 体验本来就差)
CMD_SHIM_PASSTHROUGH=1作为应急开关,需要捕获二进制输出时可临时绕过转换
安装步骤
mkdir -p ~/bin
# 把上面的脚本保存为 ~/bin/cmd
chmod +x ~/bin/cmd确认 PATH 顺序正确:
$ type cmd
cmd is /c/Users/lhw/bin/cmd
$ which -a cmd
/c/Users/lhw/bin/cmd
/c/Windows/system32/cmd第一行命中 shim,下层 cmd.exe 仍可通过绝对路径访问。
验证
5 次冷启动稳定性:
$ for i in 1 2 3 4 5; do echo -n "Run $i: "; cmd /c "echo 你好世界 测试$i"; done
Run 1: 你好世界 测试1
Run 2: 你好世界 测试2
Run 3: 你好世界 测试3
Run 4: 你好世界 测试4
Run 5: 你好世界 测试5字节级验证:
$ cmd /c "echo 你好" | xxd
00000000: e4bd a0e5 a5bd 0d0a ........e4 bd a0 = 你,e5 a5 bd = 好,完全是 UTF-8。
退出码、stderr、管道、cmd /? 中文帮助、ver 输出 [版本 10.0.19045.6466] 全部正常。
为什么写规则没用
第一直觉是"写进规则让模型避开 cmd /c,改用 PowerShell"。这条路有两个问题:
第一,规则只对会读规则的模型生效。换一个模型、换一个 agent,规则就丢了。
第二,cmd.exe 调用在很多场景下是无法避免的:第三方脚本里写死了 cmd /c,CI 工具链里嵌入了 bat 文件,Windows API 调用也可能间接走 cmd。
从环境层面解决,让 cmd 这个命令本身就吐 UTF-8,比写"不要用 cmd"的规则可靠得多。
替代方案对比
| 方案 | 可行性 | 缺点 |
|---|---|---|
命令内 chcp 65001 |
部分可行 | 首次调用有竞态,不可靠 |
| Windows Beta UTF-8 系统设置 | 真正彻底 | 需要管理员权限 + 重启,会影响所有应用 |
修改 ~/.bashrc 定义函数 |
不可行 | bash -c 非交互模式不加载 rc 文件 |
| PATH shim + iconv | 采用 | 仅在 Git Bash 内生效,二进制输出需开环境变量 |
Wrapper 命令 cmdu 等新名字 |
可行 | 需要修改所有调用方,无法拦截已有代码 |
回滚方式
rm ~/bin/cmdPATH 解析自动落回 /c/Windows/system32/cmd,恢复原始行为。无任何注册表、系统设置或全局环境变量改动。
本文由 AI 辅助生成,可能存在错误或遗漏,请以实际资料和官方文档为准。