Git Bash 执行中文命令报错 127:MSYS 参数编码 bug 排查与修复

Git for Windows 2.20.1(2018年版本)自带的 bin/bash.exe 存在 MSYS 参数编码 bug:通过 bash -c 传递包含 UTF-8 中文字符的命令时,参数内容会被破坏,导致所有中文命令报错 exit 127

解决方案:升级 Git for Windows 到最新版(2.55.0),问题消失。


起因

在使用 AI Agent(Hermes / Claude Code)的 terminal 工具时,发现一个诡异现象:任何包含中文的 shell 命令都会失败

bash 复制代码
$ echo hello
hello                          # 正常

$ echo 你好
exit code: 127                 # 失败

$ ls '/e/作业/'
exit code: 127                 # 失败

$ ls /e/作业/                   # 不加引号
(正常列出目录内容)             # 竟然成功了?

更奇怪的是,同样的命令在朋友电脑上完全正常


结论先行

经过排查,最终定位到根因:

项目 详情
根因 Git for Windows 2.20.1 的 bin/bash.exe 中,MSYS 运行时(msys-2.0.dll)将 Windows 命令行参数转换为 POSIX argv 时,破坏了 UTF-8 多字节字符
触发条件 bash -c '<含中文的参数>'
表现 参数中的换行符 \n 被转义为字面 \\n,中文字符被损坏
修复 升级 Git for Windows(2.20.1 → 2.55.0)

升级后验证:

bash 复制代码
$ git --version
git version 2.55.0.windows.2      # 从 2.20.1 升级

$ echo 你好
你好                               # 完美解决

$ ls -la '/e/作业/生产实习/'
total 880                           # 引号中文路径也没问题了

排查过程(比较长,可以直接跳过)

Step 1:控制变量 --- 确认中文是触发条件

第一反应是排除噪音。设计了一组对照实验:

命令 结果 说明
echo hello OK 纯 ASCII
echo 你好 FAIL 127 加中文
pwd && ls -la OK 复合命令,无中文
ls '/e/作业/' FAIL 127 引号包裹中文路径
ls /e/作业/ OK 不加引号反而成功

结论明确:中文是必要触发条件

最后一个现象特别值得注意 ------ 不加引号时中文路径能正常工作。这说明 bash 本身能处理中文,问题出在某个中间环节。

Step 2:分析错误输出 --- 发现换行符丢失

echo 你好 的完整输出(这是 AI Agent 的 wrapper 脚本):

bash 复制代码
/usr/bin/bash: line 7: source .../hermes-snap-xxx.sh >/dev/null 2>&1 || true
builtin cd -- /c/Users/31548 || exit 126
eval 'echo 你好'
__hermes_ec=0
{ export -p > ... && mv -f ...; } 2>/dev/null || rm -f ... 2>/dev/null || true
pwd -P > .../hermes-cwd-xxx.txt 2>/dev/null || trueexit : No such file or directory

三个异常:

  1. wrapper 脚本内容泄露到了 stdout --- 正常情况下不应该打印出来,说明 bash 解析脚本时出了问题
  2. trueexit 被合并成了 trueexit --- 两行之间的换行符丢失了
  3. echo 你好 的输出没有出现 --- 命令根本没执行到

这一步的关键收获:问题不在用户的命令,而在 bash 收到的脚本参数被破坏了

Step 3:读源码 --- 理解执行链路

AI Agent 执行 shell 命令的链路是这样的:

python 复制代码
# 1. 构造 wrapper 脚本(多行字符串)
script = "\n".join([
    "source snap.sh >/dev/null 2>&1 || true",
    "builtin cd -- /c/Users/31548 || exit 126",
    "eval 'echo 你好'",
    "__hermes_ec=$?",
    "exit $__hermes_ec"
])

# 2. 通过 bash -c 传递
subprocess.Popen(["D:/develop/Git/bin/bash.exe", "-c", script])

脚本在 Python 层面构造完全正确("\n".join() 不会丢失换行)。问题一定在 bash 收到参数之后。

Step 4:隔离测试 --- 排除 Agent 本身的 bug

直接用 Python 调用 bash -c,不经过任何 Agent wrapper:

python 复制代码
subprocess.run(['bash', '-c', 'echo line1\necho 你好\necho line3'])
# 结果: rc=0, stdout='line1\n你好\nline3'  **OK** 正常!

bash 本身没问题。 那问题在哪?

答案藏在细节里 ------ 这里的 bash 走的是 /usr/bin/bash(MSYS 内部路径),不是 D:/develop/Git/bin/bash.exe(Windows 原生路径)。

Step 5:定位到具体 bash 可执行文件

系统上有多个 bash:

路径 来源
D:/develop/Git/bin/bash.exe Git for Windows 自带的 launcher
/usr/bin/bash MSYS 内部的 bash(同一个 Git 安装内)

AI Agent 通过 _find_bash() 查找 bash 时,优先使用了 D:/develop/Git/bin/bash.exe

这个文件是 Git for Windows 的 原生 Windows launcher ,它的作用是:接收 Windows 命令行参数 → 调用 MSYS 运行时(msys-2.0.dll)转换 → 传给底层的 MSYS bash。

Step 6:对照实验 --- 锁定 bug 位置

用同一个 Python 进程,分别调用两个 bash:

python 复制代码
git_bash = "D:/develop/Git/bin/bash.exe"   # Windows launcher
msys_bash = "/usr/bin/bash"                 # MSYS bash

# Windows launcher + 中文
subprocess.run([git_bash, '-c', 'echo 你好'])
# → rc=127 **FAIL**

# MSYS bash + 中文(通过 launcher 调用)
subprocess.run([git_bash, '-c', '/usr/bin/bash -c "echo 你好"'])
# → rc=127 **FAIL**

# 直接在 MSYS 环境内调用
# (execute_code 沙箱内)
subprocess.run(['/usr/bin/bash', '-c', 'echo 你好'])
# → rc=0 **OK**

bug 在 Git/bin/bash.exe 的参数传递层,不在 bash 本身。

Step 7:找到铁证 --- stderr 中的转义痕迹

python 复制代码
subprocess.run([git_bash, '-c', 'echo 你好\necho world'], capture_output=True, text=True)

stderr 输出:

复制代码
/usr/bin/bash: line 1: $'echo 你好\\necho world': command not found

$'...' 是 bash 的 ANSI-C 引用语法。注意 \\n ------ 这不是换行符(0x0A),而是两个字符:反斜杠 + n

这意味着 msys-2.0.dll 在将 Windows 命令行转换为 POSIX argv 时,把 UTF-8 字节流搞乱了,连带把换行符也转义成了字面量。

Step 8:用 stdin 绕过验证

python 复制代码
subprocess.run([git_bash], input='echo 你好\necho world', capture_output=True, text=True)
# → rc=0, stdout='你好\nworld' **OK**

通过 stdin 传递脚本,完全绕过了 -c 参数传递路径 → 正常。

最终确认: bug 存在于 Git/bin/bash.exe 的 MSYS 运行时将 Windows 命令行 -c 参数转换为 POSIX argv 的过程中,对 UTF-8 多字节字符的处理有误。


为什么无引号的中文路径能成功?

回到最初的困惑 ------ ls /e/作业/ 能用,ls '/e/作业/' 不行。

原因是两条路径走的是不同的处理链路:

  • 有引号ls '/e/作业/' 整体作为 -c 参数的一部分传递 → 经过 msys-2.0.dll 参数转换 → 中文被破坏
  • 无引号ls /e/作业/ 中的 /e/作业/ 被 bash 直接作为文件路径处理 → 通过文件系统 API 访问 → 中文字节原样传递给 OS,不经过参数解析

完整证据链

复制代码
用户输入 "echo 你好"
    │
Hermes 构造 wrapper 脚本 (Python 字符串, 换行正确)
    │
subprocess.Popen(["D:/develop/Git/bin/bash.exe", "-c", script])
    │
Python list2cmdline() 拼成 Windows 命令行字符串
    │
CreateProcess() 启动 bash.exe
    │
bash.exe → msys-2.0.dll 解析命令行 → 转换为 POSIX argv
    │  ← 此处 UTF-8 字节被破坏,\n 变成 \\n
    │
bash 收到损坏的 -c 参数
    │
bash 执行失败 → exit 127

排查方法论

复制代码
1. 控制变量    → 确认"中文"是必要触发条件
2. 分析输出    → 发现 trueexit 合并 → 换行符丢失
3. 读源码      → 理解 wrapper 机制 → bash -c 传递
4. 隔离测试    → 绕过 Agent 直接调 bash → 正常 → 问题不在 bash 本身
5. 区分执行体  → 找到 Agent 用的 Git/bin/bash.exe
6. 对照实验    → 同一进程两个 bash,一好一坏 → 锁定 launcher
7. stdin 绕过  → 确认是 -c 参数传递路径的问题
8. 找铁证      → stderr 中 $'...'\\n' 暴露了 msys 转义 bug

核心思路:从现象出发,每一步排除一个可能性,逐步缩小嫌疑范围,直到只剩一个解释。 当遇到"同样的代码在别人电脑上能跑"的情况,优先怀疑环境差异(版本、路径、运行时)。


修复验证

项目 升级前 升级后
Git for Windows 2.20.1 (2018-12) 2.55.0 (2025)
bash 4.4.23 (pc-msys) 5.3.15 (pc-cygwin)
测试命令 升级前 升级后
echo hello OK OK
echo 你好 FAIL 127 OK
echo "中文测试" FAIL OK
ls '/e/作业/' FAIL 127 OK

升级 Git for Windows 后问题完全解决。

相关推荐
fengyehongWorld3 小时前
Git 认证凭据管理
git
暮云星影11 小时前
git版本发布
git·gitee·github·gitea
乌夷11 小时前
Git Flow 分支模型
git
独隅12 小时前
VS Code Git 工作树多分支并行开发实战评测
git
暮云星影17 小时前
git仓库分支管理
git·gitee·github·gitea
柒号华仔1 天前
「速通Shell」聚砖成墙,Shell函数封装之道
linux·ssh·bash
深念Y2 天前
Opencode Event 表写入优化方案
数据库·人工智能·ai·node.js·bug·优化·opencode
互联网中的一颗神经元2 天前
ZZ — Git 速查表
大数据·git·elasticsearch
早安试言2 天前
git使用
git
燐妤2 天前
梦绘片场 ProjectDream故事总纲场景剧本篇:从项目到成片的 AI 漫剧工作台
人工智能·git·ai·项目开发