用 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

关键设计点

安装步骤

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/cmd

PATH 解析自动落回 /c/Windows/system32/cmd,恢复原始行为。无任何注册表、系统设置或全局环境变量改动。

本文由 AI 辅助生成,可能存在错误或遗漏,请以实际资料和官方文档为准。