Windows 批处理编写规范(.bat 编码 + 行尾)
为什么需要这个技能
AI 编码工具(Write / Edit / 各类编辑器)默认输出 UTF-8 无 BOM + LF 行尾。
这两个默认值对 .bat 都是致命的:cmd.exe 有自己的读取规则,会因此报出
一堆看起来毫无道理的「幽灵命令」错误,而脚本可能"一半能跑一半报错",
极难自行定位。
触发信号:用户反馈运行 .bat 时出现
- '宸茶杩涚▼' 不是内部或外部命令 —— 中文整行乱码(编码问题)
- 'ktop' 不是内部或外部命令、'when' 不是… —— 命令名被截断(行尾问题)
- 或两者混合出现
两条铁律
| 维度 | 要求 | 违反后的症状 |
|---|---|---|
| 内容编码 | 纯 ASCII;确需中文则存 GBK(非 UTF-8) | 整行变乱码:'鍦板潃:' 不是… |
| 行尾 | CRLF(\r\n),不能是纯 LF |
整行错位截断:'ktop' 不是… |
原理:
- cmd.exe 按系统 OEM 代码页读取批处理(简体中文 = GBK/936)。
UTF-8 中文被按 GBK 解码 → 乱码 → 乱码行会被当作命令尝试执行。
- chcp 65001 对已经读入的行来不及生效,救不了场(它是运行时切换,
而文件解析在更早阶段)。
- cmd.exe 的行分割依赖 CRLF。纯 LF 会导致按字节偏移错位切行。
标准做法
1. 内容全 ASCII
注释、echo、title、错误提示全用英文。脚本丑一点无所谓,能跑最重要。
@echo off
rem ============================================================
rem my launcher - ASCII ONLY, CRLF endings
rem 1) ASCII: cmd.exe reads .bat with the OEM codepage (GBK
rem on zh-CN). UTF-8 Chinese gets mis-decoded into garbage
rem that cmd then tries to EXECUTE.
rem 2) CRLF: a LF-only .bat makes cmd mis-split lines, giving
rem truncated errors like 'ktop' / 'when'.
rem ============================================================
2. 中文路径不要硬编码
项目文件夹名是中文时(如 D:\projects\我的工具\),
不要在脚本内容里写这个路径。改用 %~dp0 自解析:
rem 从脚本自身所在目录向上查找项目根
set "PROJ="
for %%D in ("%~dp0." "%~dp0.." "%~dp0..\..") do (
if not defined PROJ if exist "%%~fD\tools\app.py" set "PROJ=%%~fD"
)
if not defined PROJ (
echo [x] Could not locate the app near this script.
goto :end
)
cd /d "%PROJ%"
> 文件名含中文是安全的(文件名不进命令解析);文件内容含中文才危险。
3. 中文名脚本用"薄壳"模式
想保留中文文件名方便识别,就让中文名文件只做转发:
启动查看器.bat(中文名,内容纯 ASCII):
@echo off
call "%~dp0launch_viewer.bat" %*
真正的逻辑放在 ASCII 文件名的 launch_viewer.bat 里。
4. 写完后必须规范化行尾与编码
Write 工具写完后,必须用 PowerShell 二次处理:
$p = "D:\path\to\script.bat"
$t = [IO.File]::ReadAllText($p)
# 先把所有行尾归一为 LF,再统一成 CRLF(幂等,避免出现 \r\r\n)
$t = $t -replace "`r`n", "`n"
$t = $t -replace "`n", "`r`n"
# 纯 ASCII 内容用 ASCII 写;含中文用 GBK(不要用 UTF8)
[IO.File]::WriteAllText($p, $t, [Text.Encoding]::ASCII)
# 含中文时改为:
# [IO.File]::WriteAllText($p, $t, [Text.Encoding]::GetEncoding("GB2312"))
校验(必做)
一次统计两个维度,期望 SOLO_LF=0 且 NON_ASCII=0:
$p = "D:\path\to\script.bat"
$b = [IO.File]::ReadAllBytes($p)
$crlf = 0; $solo = 0; $non = 0
for ($i = 0; $i -lt $b.Length; $i++) {
if ($b[$i] -eq 10) { if ($b[$i-1] -eq 13) { $crlf++ } else { $solo++ } }
if ($b[$i] -gt 127) { $non++ }
}
"CRLF=$crlf SOLO_LF=$solo NON_ASCII=$non"
# SOLO_LF 必须为 0;NON_ASCII 为 0(纯 ASCII)或为 GBK 中文(可接受,前提是 GBK 存盘)
批量校验:
Get-ChildItem "D:\proj" -Filter *.bat -Recurse | ForEach-Object {
$b = [IO.File]::ReadAllBytes($_.FullName)
$solo = 0; $non = 0
for ($i = 0; $i -lt $b.Length; $i++) {
if ($b[$i] -eq 10 -and $b[$i-1] -ne 13) { $solo++ }
if ($b[$i] -gt 127) { $non++ }
}
"{0,-40} SOLO_LF={1,-4} NON_ASCII={2}" -f $_.Name, $solo, $non
}
常见陷阱
-
for /f里用set /p询问 → 无控制台时卡死或闪退。
启动器尽量做成无交互;确有分支时用明确的默认行为。 -
for /f解析netstat拿 PID 后直接taskkill——
如果服务其实正常在跑,会把用户正在用的服务杀掉。
更稳妥:检测到端口已占用时,只打开浏览器并退出,提示"服务已在运行"。 -
RedirectStandardOutput捕获不到 bat 的@echo off输出,
用重定向方式测脚本会看到空文件。要验证脚本本身,直接在真实终端里跑。 -
git add -A会把临时文件/工作目录一并提交,
写完校验脚本要及时清理,并把工作目录加进.gitignore。
自检清单
写完一个 .bat 之后,逐条确认:
- [ ] 内容是否全 ASCII?(必须;若含中文,是否已 GBK 存盘?)
- [ ] 行尾是否 CRLF?(用校验脚本确认
SOLO_LF=0) - [ ] 是否避免了硬编码中文路径?(用
%~dp0自解析) - [ ] 是否有无交互卡死的风险(
set /p/pause位置)? - [ ] 端口检测逻辑是否会误杀正在运行的服务?
- [ ] 是否在真实终端里跑过一遍?