拓冰建站拓冰建站
首页 / 资讯中心 / 正文

SVN钩子脚本选.bat还是.cmd?关键在errorlevel解析差异

我微信群里隔三差五就有人问SVN 的钩子脚本到底存成 .bat 还是 .cmd底下回答基本是三派一派说“两者没区别随便存”一派说“.bat 是 DOS 时代的遗物.cmd 才是 Windows 时代的认准 .cmd 就完事”还有一派比较谨慎说“.bat 用 COMMAND.COM 跑.cmd 用 cmd.exe 跑”。三种说法都沾点边但都没说透。我自己维护的几台 Windows 服务器上跑了十几条 SVN 钩子从 pre-commit 检查提交日志到 post-commit 发变更通知都有。早年偷懒全存成 .bat结果被各种“玄学故障”折腾得不轻钩子有时候生效有时候不生效同一套逻辑换个仓库就报错脚本单独跑完全正常挂到 SVN 上结果全看运气。后来逐个排查才发现根子大多不在逻辑而在 .bat 和 .cmd 那一层很多开发者根本不知道的解析差异上。这篇文章我会先把这两个后缀的来龙去脉和底层差异讲清楚再给 SVN 钩子脚本的推荐选型最后附上一份可以直接抄的 pre-commit 钩子模板和调试清单。不管你是刚接手公司 SVN 的运维还是准备给自己的项目补几条钩子的开发这篇应该都能帮你少踩几个坑。1. 先搞清楚CMD 和 BAT 到底是什么关系1.1 一个来自 DOS 时代一个来自 OS/2 时期先说身世。BAT 是 batch批处理的缩写从 MS-DOS 的 COMMAND.COM 时代就有了老资历地位类似命令行里的化石级存在。CMD 文件则是 OS/2 引入的后来 Windows NT 把整套 cmd.exe 命令行环境照搬过来时也把 .cmd 这个后缀一并带上了。所以严格讲.cmd 并不是“.bat 的替代品”它在 Windows NT 之后和 .bat 是“并行的两种批处理文件”都由同一个解释器 cmd.exe 加载执行。你可以理解为同一台发动机调出了两套标定一套为了兼容祖传代码一套为了新环境而设。这就引出了第一个常见的误解“.bat 由 COMMAND.COM 执行.cmd 由 cmd.exe 执行。”在现代 64 位 Windows 上16 位子系统默认是不存在的COMMAND.COM 根本没机会出场。你双击一个 .bat系统最终走的也是 cmd.exe 这条链路。所谓“BAT 归 COMMAND.COM 管”只存在于 Windows 9x 那种老系统或者 32 位 Windows 里跑 16 位程序的特殊场景。对 99% 的开发者来说.bat 和 .cmd 的解释器是同一个这点没有悬念。1.2 “都在 cmd.exe 里跑”不等于“完全一样”很多人就是卡在这既然都是 cmd.exe 在执行那后缀名不就是摆设吗答案是——后缀名真不是摆设。cmd.exe 启动后会根据传入脚本的扩展名走两条略有差异的分支。微软自己也没把这事儿宣传得很响所以大多数开发者也就照着“没区别”的刻板印象走。关键是哪两条分支最核心的一条就是 ERRORLEVEL中文语境里常叫“返回码”“错误码”的更新策略。这一条会在 2.1 节详细拆。其他的差异还包括文件关联、PATHEXT 搜索顺序以及 16 位程序的兼容行为。下面一个个说。顺便把标题里那个“90% 的人都搞错了”提前剧透一下。绝大多数人搞错的地方不是不知道这两个后缀有历史渊源而是不知道“错误码更新机制”这件事。很多人写的 SVN 钩子失效不是逻辑写错而是脚本在 .bat 和 .cmd 下对 errorlevel 的处理方式不一样导致钩子返回了错误的退出码。2. 真正有实际影响的差异都在细节里2.1 最关键的差异ERRORLEVEL 的更新机制ERRORLEVEL 是 cmd 环境里的一个动态变量每执行一条命令它都可能被刷新。你可以用%errorlevel%读取或者用if errorlevel N判断。区别在哪在 .bat 文件里像 echo、rem、cls、goto、if、for 这类内部命令执行后不会刷新 errorlevel。它保留的是上一条“真的会改写它”的命令留下的值。在 .cmd 文件里内部命令同样会刷新 errorlevelecho 成功执行就写 0。我说个最经典的试验。写两个文件内容一模一样一个存成 test.bat一个存成 test.cmdecho off dir C:\NoSuchDir echo 脚本执行完毕 echo ERRORLEVEL%ERRORLEVEL%实际运行结果如下文件输出test.batERRORLEVEL1test.cmdERRORLEVEL0为什么test.bat 里 dir 找不存在的目录返回 1紧接着 echo 是内部命令在 .bat 里不刷新 errorlevel所以保留了 1。test.cmd 里 echo 刷新了 errorlevel成功就写 0。千万别小看这个差异。随便写两行“我在 .bat 里检查 errorlevel 明明是对的挪到 .cmd 里就全错了”这种怪事十有八九就是它引起的。举一个实际的翻车场景。你想在脚本里做个检查失败了就退出echo off some_check_command echo 检查完成 if errorlevel 1 exit /b 1 exit /b 0这段在 .bat 里如果 some_check_command 失败返回 1后面的 echo 不会改写 errorlevelif errorlevel 1成立正常退出。但在 .cmd 里echo 已经把 errorlevel 洗成 0 了if errorlevel 1永远不成立——该拦截的没拦截直接exit /b 0放行了。想象这是 SVN 的 pre-commit 钩子钩子返回 0SVN 认为校验通过坏提交就这么进仓库了。反过来在 .bat 里也可能出相反的问题某个跟校验无关的旧命令把 errorlevel 留成了非零脚本一路跑完也没走退出口最后自然结束进程带着这个非零码退出。SVN 一看非零提交被莫名拒绝用户一脸懵。这种“钩子看心情放行或拦停”的问题排查起来非常费工夫。结论先放在这无论 .bat 还是 .cmd都必须显式写exit /b 0或exit /b 1不能靠“自然结束时的残留值”。但 errorlevel 的更新语义.cmd 更符合“每条命令后都可信”的直觉这也是我最终推荐钩子用 .cmd 的核心理由后面 3.2 节再展开。2.2 影响执行效果的差异文件关联与搜索优先级Windows 默认的 PATHEXT 顺序是.COM;.EXE;.BAT;.CMD;.VBS;...。注意.BAT排在.CMD前面。这意味着如果同一个目录下同时有 build.bat 和 build.cmd你在命令行敲build系统实际执行的是 build.bat。不少项目就因为这个“撞名”莫名其妙地跑了旧脚本。文件关联上二者默认都关联到 cmd.exe双击都能运行右键都有“编辑”。但如果你改过系统代码页、文件关联或者装过某些安全软件和终端模拟器关联顺序也可能变。有这种环境洁癖的团队最好在项目文档里写明统一使用哪个后缀避免同名文件互相遮蔽。还有一个常见但基本过时的说法“16 位程序只能跑 .bat跑不了 .cmd。”严格说这在远古环境里是成立的COMMAND.COM 不认识 .cmd。但现代 64 位 Windows 默认没有 16 位子系统根本轮不到 COMMAND.COM 出场。只有在 32 位 Windows 上通过 16 位程序去触发脚本时.bat 才会被 COMMAND.COM 接管.cmd 会直接执行失败。这种场景在 2020 年代已经不常见了但真遇到老系统、老构建工具时也算是一个排查线索。2.3 别被网上传言带偏解析规则真的差很多吗除了 errorlevelcmd.exe 在解析 .bat 和 .cmd 时还有没有别的差异网上流传的很多说法比如“特殊字符在 .bat 和 .cmd 里解析规则完全不同”“.cmd 支持的命令更多”我实测下来大部分是玄学。setlocal、endlocal、延迟展开!var!、for /f、call、%~dp0、%*这些在两个后缀下行为基本一致。日常脚本里能感受到的实质差异就是 2.1 的 errorlevel再加上 2.2 的优先级和 16 位兼容。所以别再花时间去研究那些“奇谈”了把 errorlevel 这条弄明白你的批处理水平已经超过一大批人。2.4 一张表把差异说清楚对比维度.bat.cmd历史来源MS-DOS 的 COMMAND.COMOS/2 与 Windows NT现代 Windows 的执行者cmd.execmd.exe16 位环境下的执行COMMAND.COM 可直接解析只能 cmd.exe16 位环境无法直接跑echo/rem/cls 等内部命令是否刷新 errorlevel不刷新刷新PATHEXT 默认顺序排在 .cmd 前面排在 .bat 后面适合的场景简单脚本、老项目兼容依赖执行结果和错误码的脚本、钩子3. 回到正题SVN 钩子脚本到底该用哪个3.1 SVN 钩子是怎么被调用的先复习下 SVN 钩子的机制不然下面的建议都是空中楼阁。SVN 仓库下面有个 hooks 目录里面放了一堆可执行脚本文件名是固定的start-commit、pre-commit、post-commit、pre-revprop-change、post-revprop-change 等。SVN 服务在特定时机去调用对应脚本把参数传进来再看返回码定乾坤。以用得最多的 pre-commit 为例第一个参数是仓库路径第二个参数是事务名txnSVN 还会设置SVN_REPOS、SVN_TXN这两个环境变量不同服务端实现可能略有出入但主流版本都会设脚本返回 0提交放行返回非 0提交被中止同时脚本的 stderr 输出会原样发给客户端用户能直接看到拒绝原因脚本的 stdout 在提交被拒绝时通常不会显示所以要在脚本里把错误信息重定向到 stderr也就是echo 错误信息 12。Windows 上跑这套钩子常见有两种方式一是装 VisualSVN Server在管理界面配二是用 svnserve 以服务方式跑。无论哪种钩子脚本最终都是被 SVN 服务进程调用。这里有个容易忽略的点服务进程的环境变量、当前目录、代码页都跟你在 cmd 窗口手动跑脚本时不一样。很多人“手动复现不了问题”就是因为忽略了“服务环境”这个变量。3.2 为什么我推荐 .cmd错误码语义更可预期先给结论新写的 SVN 钩子用.cmd。原因不是“.cmd 比 .bat 高级”而是三条很实际的理由第一errorlevel 语义一致、可预期。在 .cmd 里你可以在任何一条命令后面随手echo %errorlevel%看状态不会因为 echo 那句本身把值糊过去。这一点在我排查钩子问题时的体感差别非常大。在 .bat 里你插一句调试用的 echo反而可能把原本的错误码掩盖掉越调越乱。第二与 PowerShell、C# 等现代工具的“每条命令后都可读错误码”直觉一致换人来维护时不容易写错。第三钩子里大量用到if errorlevel、%errorlevel%、exit /b在 .cmd 下的行为更线性。但要强调后缀只是必要条件显式exit /b才是充分条件。.cmd 只是把“忘记显式退出”的后果变得更符合直觉并不是包治百病。所以我推荐的写法里每条分支都会显式退出绝不依赖自然结束时的残留错误码。顺便给选型党一个反向参考如果按“失败模式”来看.bat 因为残留 errorlevel容易在脚本自然结束时带着非零码退出表现是“该过的不给过”.cmd 因为内部命令清零容易在脚本自然结束时返回 0表现是“该拦的没拦住”。从安全审计角度前者更像“宁可错杀”后者更像“先放行再说”。但我的观点很明确钩子脚本就该显式退出把“靠自然结束赌命”这种写法从团队里禁掉。在这个前提下选 .cmd。3.3 可以直接抄的 pre-commit 钩子模板.cmd下面这个模板是我目前在用的简化版逻辑覆盖了“日志必须填写”“日志不能太短”“必须能获取到作者”三个最常见场景echo off setlocal rem 防止路径带空格导致参数截断用 %~1 去掉 svn 传来的外层引号再统一加引号 set REPOS%~1 set TXN%~2 rem 处理 UTF-8 输出中文日志和提示都能正常显示 chcp 65001 nul rem svnlook 建议写全路径别赌服务环境的 PATH set SVNLOOKC:\Program Files\VisualSVN Server\bin\svnlook.exe rem 1. 日志不能为空 %SVNLOOK% log %REPOS% -t %TXN% | findstr . nul if errorlevel 1 ( echo 提交被拒绝必须填写日志信息。 12 exit /b 1 ) rem 2. 日志至少 8 个字节英文日志按字符算中文日志按字节算严格需求请用外部脚本 %SVNLOOK% log %REPOS% -t %TXN% | findstr /R ........ nul if errorlevel 1 ( echo 提交被拒绝日志信息过短至少 8 个字符。 12 exit /b 1 ) rem 3. 必须能获取到作者 %SVNLOOK% author %REPOS% -t %TXN% nul 21 if errorlevel 1 ( echo 提交被拒绝无法获取作者信息。 12 exit /b 1 ) exit /b 0逐个说下关键点。set REPOS%~1和set TXN%~2是处理路径空格的关键。SVN 传参时仓库路径可能带引号也可能带空格%~1会把外层引号剥掉拿到干净字符串后面使用的时候再统一加引号。如果你直接用%1然后%1遇到带引号的参数会变成C:\My Repocmd 解析起来很脆弱早晚翻车。chcp 65001 nul是为了统一代码页。svnlook 输出的日志是 UTF-8 字节流而 SVN 服务进程的默认代码页通常是系统 ANSI 代码页中文系统常见 936。不统一代码页的话findstr 去匹配中文字节很容易出问题echo 中文提示也可能乱码。注意一个配套要求如果你要输出中文提示脚本文件本身要保存为 UTF-8 无 BOM。带 BOM 的 UTF-8 在 cmd 环境里偶尔会把 BOM 当成第一个命令的一部分产生“不是内部或外部命令”的假报错别在这种细节上赌运气。想省心的话把提示全部写成英文也行。findstr .检查日志是否为空findstr /R ........检查是否有 8 个字节以上的内容。这里有个诚实的提醒findstr 对中文多字节的处理在不同版本 Windows 上表现不一致严格说这个检查是“字节数”而不是“字符数”。如果团队里主要用英文日志这个检查完全够用如果必须精确校验中文日志字数就别在批处理里硬抠改成调用 Python 或 PowerShell 做更严格的判断。所有错误提示都加了12确保信息走 stderrSVN 客户端才能看到。很多钩子“不报错”的假象其实就是因为错误信息写到了 stdout被服务端吞掉了。3.4 再给一个 post-commit 通知脚本post-commit 的职责是在提交成功后做通知或记录即使钩子失败也不会阻止提交所以写法上更宽松但也别在钩子里做重活否则客户端会一直等到脚本跑完。echo off setlocal set REPOS%~1 set REV%~2 set SVNLOOKC:\Program Files\VisualSVN Server\bin\svnlook.exe set LOGFILEC:\svn-hooks\post-commit.log ( echo [%date% %time%] repo%~1 rev%~2 %SVNLOOK% changed %REPOS% -r %REV% ) %LOGFILE% 21 exit /b 0这里的%date% %time%依赖系统区域设置不同系统格式不一样看个大概时间没问题。真要精确到毫秒或者想跨系统一致还是建议用外部脚本。svnlook changed -r %REV%会列出本次提交所有变更路径追加到日志文件里之后想统计、想发邮件都能基于这个日志做。4. 实操验证与排查技巧4.1 用一个实验把 .bat 和 .cmd 的真实差异钉死光说不练假把式。新建两个文件内容完全一样一个存成 test.bat一个存成 test.cmdecho off dir C:\ThisDirDoesNotExist nul 21 echo 上面那条命令失败了吗你猜猜 echo errorlevel%errorlevel%直接双击运行或者从另一个 cmd 窗口用cmd /c test.bat和cmd /c test.cmd看输出结果是test.baterrorlevel1test.cmderrorlevel0再换一种写法让最后一条命令是“真正会设置错误码的命令”echo off dir C:\ThisDirDoesNotExist nul 21 ver nul echo errorlevel%errorlevel%此时 .bat 和 .cmd 都会显示 0因为 ver 成功执行刷掉了错误码。这个小对比能让团队里的人直观理解“echo 会不会污染错误码”才是 .bat 和 .cmd 的分水岭。另外还有个肉眼可见的差异如果你在脚本里写了chcp 65001脚本本身又带中文保存编码和代码页不匹配时echo 出来的中文会变成乱码。这种问题跟后缀名无关纯粹是编码配套没做好。4.2 钩子脚本的调试三板斧SVN 钩子没法在 IDE 里直接调我常用的调试三板斧如下。第一板斧手工模拟参数。在仓库目录下手动运行cmd /d /c D:\Repos\myrepo\hooks\pre-commit.cmd D:\Repos\myrepo 42注意第二个参数在真实验收时是事务名而不是版本号手工测试可以用任意字符串糊弄。跑了之后立刻查返回码echo %errorlevel%返回 1 且窗口打印了你的错误提示说明脚本本身工作正常剩下的问题在 SVN 服务端配置。第二板斧确认 svnlook 路径。怕 svnlook 找不到先在命令行确认svnlook --version。VisualSVN Server 安装后svnlook 通常在安装目录的 bin 子目录下钩子脚本里建议把路径写全set SVNLOOKC:\Program Files\VisualSVN Server\bin\svnlook.exe %SVNLOOK% log %REPOS% -t %TXN%写死完整路径虽然丑但能少很多环境变量坑。SVN 服务进程的环境变量和你终端里的未必相同PATH 里有没有 svn 目录全看运气。第三板斧开服务端日志。VisualSVN Server 有日志功能把日志级别调到 debug能看到钩子的调用记录和返回码。这招用来定位“钩子压根没被调用”比什么都有用。还有一条容易被忽略的权限检查。钩子脚本是服务账户执行的路径中间每一层目录都要允许服务账户访问。Windows 上最常见的是仓库放在 D 盘而 D 盘某个父目录被安全软件锁了权限服务进程读不到 hooks 目录钩子自然哑火。4.3 常见问题速查表症状常见原因处理办法钩子根本没执行脚本名不对必须是 pre-commit.cmd 等固定名服务没重启目录权限不够核对文件名重启服务检查服务账户对 hooks 目录的权限提交被莫名拒绝.bat 残留 errorlevel脚本里存在无关命令改写错误码改用 .cmd所有分支显式 exit /b该拦的没拦住.cmd 里 echo 等命令把 errorlevel 清零校验后未立即判断检查if errorlevel是否紧跟目标命令或先用set rc%errorlevel%保存错误提示客户端看不到错误信息写到 stdout 而非 stderrecho 错误信息 12中文提示乱码脚本编码与代码页不一致svnlook 输出是 UTF-8 而控制台不是文件存 UTF-8 无 BOM脚本开头chcp 65001 nul路径含空格时 svnlook 报错直接用 %1 未重新加引号用set REPOS%~1后统一%~1/%REPOS%找不到 svnlook服务环境 PATH 不含 svn 安装目录写死完整路径或配置环境变量钩子执行但 svnlook 结果不对当前目录不是仓库-t 事务名写错在钩子里用绝对路径确认参数顺序这些坑我基本都踩过一轮。最隐蔽的是“错误提示客户端看不到”那条很多人以为钩子没拦截成功其实是拦截了但信息写到 stdout 被吞了客户端那边只看到一句干巴巴的“Commit blocked by pre-commit hook”没有具体原因排查半天还以为逻辑没生效。5. 收尾几条亲测有效的维护习惯最后分享几个我个人的维护习惯都是在生产环境里被坑出来的。第一一律 .cmd 加显式exit /b。就算脚本只有三行也习惯性地把每条分支的返回码写清楚。这条习惯帮我挡掉了至少三次生产事故包括一次同事把日志校验钩子从 .bat 改成 .cmd 后因为没有显式退出让一堆空日志提交溜进了仓库。第二钩子脚本本身也是代码一定要放到版本管理里最好单独建一个仓库不要和代码仓库混在一起改了之后同步到服务器。我吃过没有版本管理的亏某天发现 pre-commit 钩子逻辑和我记忆里完全不一样最后才知道是同事直接在服务器上改的没有任何记录想回滚都找不到上一版。第三排查钩子问题永远从“链路通不通”开始。在 hooks 目录下放一个只含一行exit /b 0的最小脚本先用它验证 SVN 调用链路是否通畅再逐步把逻辑加回来。先证明“SVN 能调到脚本”再谈“脚本逻辑对不对”顺序反了会浪费大量时间。第四中文团队一定要提前约定编码策略。我的建议是钩子提示信息全部用英文或者统一 UTF-8 无 BOM 加chcp 65001二选一别混着来。混用编码的钩子脚本在服务进程那种环境里最容易出玄学乱码。这几个习惯看着不起眼但真的能让 SVN 钩子从“三天两头找 Sigma”变成“部署完就忘了它存在”。希望这篇文章能帮你把 .bat 和 .cmd 的迷雾一次拨开以后写钩子心里有底。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门