cmd 读批处理时,文件里只要有多字节字符,后续行就可能从错位的地方开始读,报出 is not recognized 和半截命令名;我们试过 chcp 65001、带 BOM 的 UTF-8、GBK,只有纯 ASCII 稳定。同一个坑在 Autarkia 里出现了三次,因为检查器只管一个目录、也不在 CI 里。
Windows 的 cmd 解释 .bat 时,文件里一旦有多字节字符(中文注释、中文 echo),后面的行就可能从错位的地方被读出来,报错里是被截掉开头的命令名。我们做过对照:文件开头 chcp 65001、UTF-8 带 BOM、存成 GBK,三种都还会漂移,只有内容是纯 ASCII 时和代码页无关、完全稳定。cmd 内部为什么会算错位置,我没有找到可靠的出处,这里只写观察到的现象。
第一次是一个启动脚本,报错长这样:
'ain.py' is not recognized as an internal or external command,
'ASE_DIR' is not recognized as an internal or external command,main.py 被读成 ain.py,BASE_DIR 被读成 ASE_DIR,开头各丢了一个字符。当时一起查出来两个和编码无关、但同样会让块结构错乱的写法:
if exist "%VENV%\Scripts\python.exe" (
echo Using venv (core)
rem ↑ 这个 ) 会把 if 块提前关掉,下一行跑到块外
:: 块内用 :: 当注释也会出问题,它其实是标签语法
)cmd 在解析括号块时不看 echo 后面是不是普通文本,遇到 ) 就当成块结束;:: 是一个永远不会被 goto 的标签,放在块里会破坏块的解析,块内注释要用 rem。
修的时候还遇到一个反例。有个脚本里 echo ... (%RETRY_COUNT%/%MAX_RETRIES%) 的右括号恰好提前关掉了外层的 if 块,让后面的 goto 落在块外、变量到运行时才展开。把这个括号删掉,块变完整了,变量反而在解析期就被展开成空,报 goto was unexpected at this time。这个脚本只能先列为例外,要根治得把重试循环移出 if 块。
01为什么出现了三次
第一次修完,6 个启动脚本改成了纯 ASCII,还加了一个检查器:纯 ASCII、无 BOM、块内 echo 没有括号、块内没有 ::。反向验证时把修复前的文件喂给它,都能抓到。
一个月后,根目录的 start.bat 加了几行中文注释,双击就崩,报 'p_code_sync' is not recognized,code_sync.py 被截成了半截。检查器没拦住,因为它只扫 start_scripts/ 这一个目录,而且它是个手动运行的验证脚本,不在 CI 里。这次排查还绕了一段路:我导出文件看的时候发现是 CRLF,以为是换行符的问题,后来发现是 PowerShell 的 Out-File 把输出转成了 CRLF,仓库里的文件一直是 LF。
写这篇的时候我又扫了一遍仓库,第三次已经在 HEAD 上了:一次全仓改名的提交给 start_demo.bat 加了一行中文 rem,文件第 4 行就是 chcp 65001。同一行改名还把「新变量名优先、旧变量名兼容」的两个名字改成了同一个,旧名兼容其实已经没了,只是没人发现。
这个规则本身记住了没用,它会被别人、被批量改名、被三个月后的自己打破。我给主项目留的修法是把检查器改成扫 git ls-files '*.bat'、做成普通单元测试进 CI,再加一行 .gitattributes 统一换行:
*.bat text eol=crlf仓库里现在的 .bat 有的是 CRLF、有的是 LF。LF 结尾的批处理在 goto 和 call :label 上有已知风险,我们这几次的漂移不是它引起的(纯 ASCII 的 LF 版本跑起来没问题),统一成 CRLF 只是预防。