当你在 PowerShell 里调一个 Windows 批处理脚本,把中文标题当作命令行参数传进去,理想情况是程序拿到完整的中文;现实常常是一串问号 ??,或者子进程直接报错 '??' is not recognized,连启动日志都没打印就退出了。这个看起来像编码的小问题,背后其实是一条 Windows 进程模型的硬边界。本文把这条边界讲清楚,并给出一个在本地发布流水线里验证过的稳定解法。
现象 中文参数变成问号
最直观的复现是这一组动作:在 PowerShell 5.1 里定义中文变量,再用 & 调用一个 .cmd 脚本,把中文当作位置参数传进去。脚本里只是 echo %1,期望打印原文,结果屏幕上出现的是 ??。
bash
$title = "中文标题"
& "C:\path\to\publish.cmd" $title
:: 脚本里 echo %1 -> 输出 ??
更隐蔽的是,有些程序根本不报乱码,而是直接崩溃。同样是中文参数,换成带特殊符号的标题(比如冒号)后,程序在打印任何启动日志之前就以非零码退出,输出文件空空如也。两种表现,根因同源。
根因 命令行是字节流不是字符串
Windows 创建进程时,传给 CreateProcess 的命令行是一个窄字符(ANSI)字节串,不是 Unicode 字符串。问题就出在这条字节流跨 shell 边界时的解释方式上。
当参数从 PowerShell 5.1 流向 cmd.exe,再流向最终的程序(比如一个 Electron 打包的可执行文件)时,这些字节会按系统 code page 被重新解释。在中国区机器上,code page 通常是 GBK(CP936)。如果你的中文在源头是 UTF-8 编码的字节,却在接收端被当作 GBK 解码,一个 UTF-8 汉字占 3 字节,GBK 把它拆成多个错误码元,多字节序列对不上,结果就是 ? 或者干脆是非法序列被丢弃。
rust
源头 UTF-8 字节 -> 中间按 GBK 解码 -> 乱码 截断
E4 B8 AD 文 -> GBK 解释失败 -> ?
而进程环境变量块是另一套机制 。Windows 的环境块是以 UTF-16 宽字符(Unicode)形式整块交给子进程的,不经由 code page 的窄字符转换。换句话说,你用 set 写进去的中文,子进程用 %VAR% 读出来时,字节层面就是原样的宽字符,不会被重新解释成 GBK。这正是绕开乱码的关键。
解法 把中文塞进环境变量
既然命令行字节流不可靠,就把中文从 argv 挪到环境变量里传。父进程设置变量,子进程脚本读取变量,全程不经过窄字符命令行的重解释。
PowerShell 一侧设置:
powershell
$env:J_TITLE = "Windows 中文传参坑 命令行乱码变量来救场"
$env:J_CATEGORY = "开发工具"
$env:J_FILE = "C:\path\to\article.md"
Remove-Item Env:ELECTRON_RUN_AS_NODE -ErrorAction SilentlyContinue
& "C:\path\to\mmcli_env.cmd"
批处理脚本一侧,直接读 %J_TITLE%,不再把中文写进命令行字面量:
bat
@echo off
chcp 65001 >nul
setlocal
set "ELECTRON_RUN_AS_NODE="
"%EXE%" --no-sandbox cli publish-article -p "%J_PLATFORM%" ^
--phone "%J_PHONE%" -t "%J_TITLE%" --category "%J_CATEGORY%" --file "%J_FILE%"
这样中文标题走的是 UTF-16 环境块,子进程拿到的就是干净的中文,命令行里只剩下 ASCII 开关和变量引用,GBK 再也咬不到它。
实战 一个稳定的发布封装
把上面的思路固化成一个纯 ASCII 注释的封装脚本,有几个细节必须守住。第一,脚本自身的注释要用 ASCII,因为 cmd.exe 会按 GBK 读取 .cmd 文件,中文注释一旦被错误解码,可能破坏后续命令。第二,要清掉 ELECTRON_RUN_AS_NODE 这类会被父环境带进子进程的变量,否则 Electron 程序会退化成 node 去加载脚本而报模块缺失。第三,输出重定向到文件,因为发布程序会开图形窗口,前台等待容易被沙箱回收。
bat
@echo off
chcp 65001 >nul
setlocal
set "ELECTRON_RUN_AS_NODE="
set "DIR=%~dp0"
if "%MMCLI_OUT%"=="" set "MMCLI_OUT=%DIR%mmcli-out.txt"
if "%J_TITLE%"=="" ( echo missing title > "%MMCLI_OUT%" & exit /b 2 )
"%EXE%" --no-sandbox --disable-gpu-sandbox cli publish-article ^
-p "%J_PLATFORM%" --phone "%J_PHONE%" -t "%J_TITLE%" ^
--category "%J_CATEGORY%" --file "%J_FILE%" > "%MMCLI_OUT%" 2>&1
exit /b %ERRORLEVEL%
延伸 标题里的冒号也危险
即使中文乱码修好了,命令行参数里还有另一个隐形炸弹:冒号。无论是半角 : 还是全角 :,一旦出现在标题里并经环境变量重建命令行,程序可能在打印任何启动日志之前就崩溃,退出码为负、输出文件空。冒号在很多 shell 和命令行解析器里是特殊分隔符(驱动器符、标签、参数分隔),它破坏的不只是显示,而是命令行结构本身。
最稳妥的做法是标题一律用空格或顿号替代冒号。比如"局部重绘:修掉穿帮帧"写成"局部重绘 修掉穿帮帧"。这既避开了命令行解析风险,也避开了部分平台对标题标点的审核偏好。
复盘 三条纪律
回到工程现场,这类问题可以压缩成三条可复用的纪律。其一,跨 shell 传中文,优先走环境变量而非命令行 argv,因为环境块是 UTF-16,命令行是窄字符字节流,后者必然受 code page 影响。其二,批处理脚本的注释保持 ASCII,避免 cmd.exe 按 GBK 误读破坏脚本。其三,凡是会进入命令行的标题类参数,禁用冒号,用空格或顿号替代。
这三条在一条每天自动发文的本地流水线上已经跑了不少轮,零乱码、零崩溃。编码问题的本质从来不是显示不对,而是数据在哪一层的字节解释被改写了,想清楚这一层,乱码就不再是玄学。