问题描述
Windows 批处理编程是否支持异常处理?如果没有,有没有办法有效地模拟批处理文件中的异常处理?
我希望能够在批处理脚本中的任何位置抛出异常",在任何 CALL 级别,并重复弹出 CALL 堆栈,直到找到活动的TRY 块",然后CATCH 块"可以完全处理异常并继续,或者做一些清理并继续弹出 CALL 堆栈.如果从不处理异常,则批处理终止,控制权返回命令行上下文,并显示一条错误消息.
已经有几种已发布的方法可以在任何 CALL 深度终止批处理,但这些技术都不允许任何通常通过异常处理在其他语言中提供的结构化清理活动.
注意: 在这种情况下,我已经知道一个最近才发现的好答案,我想分享信息
Windows 批处理脚本当然没有任何正式的异常处理 - 考虑到该语言的原始程度,这不足为奇.在我最疯狂的梦想中,我从未想过可以破解有效的异常处理.
但随后在一个俄罗斯网站上有了一些惊人的发现,关于错误的 GOTO 语句的行为(我不知道说什么,我看不懂俄语).在 DosTips 上发布了英文摘要,行为进一步调查.
事实证明,(GOTO) 2>NUL
的行为几乎与 EXIT/B 相同,除了在有效返回后仍会执行已解析代码块中的连接命令, 在调用者的上下文中!
这是一个简短的例子,展示了大部分的要点.
@echo offsetlocal enableDelayedExpansion设置var=父值"(调用:测试echo 这行和下面这行不执行退出/b):休息我是怎么到这里的^^!^^!^^!^^!退出/b:测试setlocal disableDelayedExpansion设置var=子值"(goto) 2>nul &echo var=!var!&转到:中断echo 这行不执行:休息echo 这行不执行
-- 输出 --
var=父值我怎么到这里了!!!!
这个功能完全出乎意料,而且非常强大和有用.它已被用于:
- 创建 PrintHere.bat - 模拟'nix here 文档功能
- 创建一个 RETURN.BAT 实用程序,任何批处理函数"可以方便地调用以跨越 ENDLOCAL 屏障返回任何值,几乎没有限制.该代码是 jeb 原始想法的充实版本.
现在我还可以向列表中添加异常处理:-)
该技术依赖于名为 EXCEPTION.BAT 的批处理实用程序来定义用于指定 TRY/CATCH 块以及抛出异常的环境变量宏".
在实现 TRY/CATCH 块之前,必须使用以下命令定义宏:
调用异常初始化
然后使用以下语法定义 TRY/CATCH 块:
:调用例程设置本地%@尝试%REM 正常代码放在这里%@EndTry%:@抓住REM 异常处理代码放在这里:@EndCatch
可以通过以下方式随时抛出异常:
调用异常 throw errorNumber "messageString" "locationString"
当抛出异常时,它使用 (GOTO) 2>NUL
迭代地弹出 CALL 堆栈,直到找到活动的 TRY/CATCH,然后它分支到 CATCH 块并执行该代码.一系列异常属性变量可用于 CATCH 块:
- exception.Code - 数字异常代码
- exception.Msg - 异常消息字符串
- exception.Loc - 描述抛出异常位置的字符串
- exception.Stack - 一个字符串,从 CATCH 块(或命令行,如果没有被捕获)跟踪调用堆栈,一直到异常源.
如果异常处理完毕,则应通过call exception clear
清除异常,脚本正常运行.如果异常没有完全处理好,那么可以抛出一个新的异常,用一个全新的异常堆栈,或者用
调用异常重新抛出 errorNumber "messageString" "locationString"
如果没有处理异常,则打印未处理异常"消息,包括四个异常属性,终止所有批处理,并将控制权返回到命令行上下文.
这是使这一切成为可能的代码 - 完整的文档嵌入在脚本中,并可通过 exception help
或 exception/?
从命令行获得.>
EXCEPTION.BAT
::EXCEPTION.BAT 1.4 版:::: 为 Windows 批处理脚本提供异常处理.:::: 由 Dave Benham 设计和编写,并得到了来自:: DosTips 用户 jeb 和 siberia-man:::: 完整文档位于此脚本的底部:::: 历史::: v1.4 2016-08-16 改进检测命令行延迟扩展:: 使用 jeb 的原创想法:: v1.3 2015-12-12 通过 MORE 添加分页帮助选项:: v1.2 2015-07-16 使用 COMSPEC 而不是 OS 来检测延迟扩展:: v1.1 2015-07-03 保留!在启用延迟扩展时的异常属性中:: v1.0 2015-06-26 带有嵌入式文档的初始版本::@回声关闭如果 "%~1" 等于 "/??"转到分页帮助如果 "%~1" 等于 "/?"去帮助if "%~1" equ "" goto help移位/1 &转到 %1:throw errCode errMsg errLoc设置异常.堆栈=":: 失败到 :rethrow:rethrow errCode errMsg errLocsetlocal disableDelayedExpansion如果未定义异常.重新启动设置exception.Stack=[%~1:%~2] %exception.Stack%"for/f "delims=" %%1 in ("%~1") do for/f "delims=" %%2 in ("%~2") do for/f "delims=" %%3 in ("%~3") 做 (setlocal enableDelayedExpansionfor/l %%# in (1 1 10) do for/f "delims=" %%S in (" !exception.Stack!") do ((转到) 2>NULsetlocal enableDelayedExpansion如果 "!!"等号 "" (本地端setlocal disableDelayedExpansion调用集funcName=%%~0"调用集batName=%%~f0"如果定义了 exception.Restart (set "exception.Restart=") else call set "exception.Stack=%%funcName%%%%S"setlocal EnableDelayedExpansion如果 !exception.Try!== !batName!:!funcName!(本地端本地端设置异常.代码=%%1"如果 "!!"等号 "" (调用 "%~f0" setDelayed) 别的 (设置异常.Msg=%%2"设置异常.Loc=%%3"设置异常.堆栈=%%S")设置异常.尝试="(称呼 )转到:@Catch)) 别的 (对于 (Code Msg Loc Stack Try Restart) 中的 %%V,请设置exception.%%V="如果 "^!^" 等于 "^!"(调用 "%~f0" showDelayed) 别的 (回声(echo 未处理的批处理异常:回声代码 = %%1回声消息 = %%2回声位置 = %%3回声堆栈=%%S)回声调用 "%~f0" 杀死)>&2)设置异常.重启=1setlocal disableDelayedExpansion调用 "%~f0" 重新抛出 %1 %2 %3):: 永远不会到达这里:在里面设置@Try=调用设置异常.Try=%%~f0:%%~0"设置@EndTry=set"exception.Try=" & goto :@endCatch":: 跌倒到 :clear:清除对于 %%V in (Code Msg Loc Stack Restart Try) 设置exception.%%V="退出/b:Kill - 停止所有处理,忽略任何剩余的缓存命令setlocal disableDelayedExpansion如果不存在 "%temp%Kill.Yes" 调用:buildYes调用 :CtrlC nul 2>&1:CtrlC@cmd/c 退出 -1073741510:buildYes - 为操作系统使用的语言建立一个 Yes 文件推送%temp%"设置是="复制 nul Kill.Yes >nul对于/f "delims=(/tokens=2" %%Y in ('"copy/-y nul Kill.Yes <nul"') 如果没有定义 yes set "yes=%%Y"echo %yes%> Kill.Yes弹出退出/b:设置延迟setLocal disableDelayedExpansion为了 %%.在 (.) 做 (设置v2=%%2"设置v3=%%3"设置vS=%%S")(本地端设置exception.Msg=%v2:!=^!%"设置exception.Loc=%v3:!=^!%"设置 "exception.Stack=%vS:!=^!%")退出/b:showDelayed -setLocal disableDelayedExpansion为了 %%.在 (.) 做 (设置v2=%%2"设置v3=%%3"设置vS=%%S")for/f "delims=" %%2 in ("%v2:!=^!%") 为/f "delims=" %%3 in ("%v3:!=^!%") do for/f "delims=" %%S in ("%vS:!=^!%") 做 (本地端回声(echo 未处理的批处理异常:回声代码 = %%1回声消息 = %%2回声位置 = %%3回声堆栈=%%S)退出/b:-?:帮助setlocal disableDelayedExpansion对于/f "delims=:" %%N in ('findstr/rbn "::::DOCUMENTATION:::" "%~f0"') 设置 "skip=%%N"for/f "skip=%skip% tokens=1* delims=:" %%A in ('findstr/n "^" "%~f0"') 做 echo(%%B退出/b:-??:pagedHelpsetlocal disableDelayedExpansion对于/f "delims=:" %%N in ('findstr/rbn "::::DOCUMENTATION:::" "%~f0"') 设置 "skip=%%N"((for/f "skip=%skip% tokens=1* delims=:" %%A in ('findstr/n "^" "%~f0"') do @echo(%%B)|more/e) 2> 空退出/b:-v:/v:版本回声(对于/f "delims=:" %%A in ('findstr "^::EXCEPTION.BAT" "%~f0"') 做 echo %%A退出/b:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::文档:::EXCEPTION.BAT 是一个纯批处理脚本实用程序,提供强大的异常在批处理脚本中处理.它使代码能够放置在 TRY/CATCH 块中.如果没有抛出异常,则只执行 TRY 块内的代码.如果抛出异常,则重复弹出批处理 CALL 堆栈,直到它到达一个活动的 TRY 块,此时控制被传递给相关联的CATCH 块和正常处理从该点恢复.CATCH 中的代码除非抛出异常,否则块将被忽略.异常可能会在与引发异常的脚本不同的脚本中捕获.如果在抛出异常后没有找到活动的 TRY,那么一个未处理的异常消息被打印到 stderr,所有处理在当前 CMD shell,并且控制返回到 shell 命令行.TRY 块是使用宏指定的.显然必须定义宏在它们可以使用之前.TRY 宏是使用以下 CALL 定义的调用异常初始化除了定义@Try 和@EndTry,init 例程还明确地清除任何先前处理可能留下的剩余异常.TRY/CATCH 块的结构如下:%@尝试%REM 任何正常代码都放在这里%@EndTry%:@抓住REM 异常处理代码放在这里:@EndCatch- 每个 TRY 必须有一个关联的 CATCH.- TRY/CATCH 块不能嵌套.- 任何使用 TRY/CATCH 的脚本或 :labeled 例程必须至少有一个SETLOCAL 在第一个 TRY 出现之前.- TRY/CATCH 块使用标签,因此不应将它们放在括号内.可以做到,但是在将控制权传递给时括号块被破坏:@Catch 或 :@EndCatch 标签,代码变得难以解释和维护.- 可以在 TRY 或 CATCH 块中使用任何有效代码,包括 CALL、GOTO、:labels 和平衡括号.但是,GOTO 不能用于离开尝试块.如果标签出现,GOTO 只能在 TRY 块中使用在同一个 TRY 块中.- GOTO 绝不能将控制权从 TRY/CATCH 外部转移到 TRY 或捕捉块.- 不应使用 CALL 来调用 TRY 或 CATCH 块中的标签.- 包含 TRY/CATCH 的 CALLed 例程必须具有唯一的标签剧本.无论如何,这通常是很好的批处理编程实践.不同的脚本可以共享 :label 名称.- 如果脚本或例程递归调用自身并包含 TRY/CATCH,则在执行第一个 %@Try% 之前它不能抛出异常通过使用抛出异常调用异常抛出代码消息位置在哪里代码 = 异常的数字代码值.消息 = 异常的描述.Location = 帮助识别异常发生位置的字符串.可以使用任何值.一个好的通用值是%~f0[%~0]",它扩展到当前执行的完整路径脚本,后跟当前正在执行的例程名称方括号内.如果 Message 和 Location 值包含空格或毒药,则必须引用它们像 & 这样的字符|<>.这些值不得包含额外的内部引号,并且它们不能包含插入符号 ^.将定义以下变量以供 CATCH 块使用:exception.Code = 代码值exception.Msg = 消息值exception.Loc = 位置值exception.Stack = 从 CATCH 块(或命令行)跟踪调用堆栈如果没有被捕获),一直到异常.如果未捕获到异常,则所有四个值都将作为未处理的异常"消息,并且未定义异常变量.CATCH 块应始终在最后执行以下操作之一:- 如果异常已经处理并且可以继续处理,则清除使用异常定义调用异常清除绝不能在 Try 块中使用 Clear.- 如果异常还没有被完全处理,那么应该创建一个新的异常抛出可以被更高级别的 CATCH 捕获.你可以扔一个新的使用正常 THROW 的异常,这将清除 exception.Stack 和任何更高的 CATCH 将不会意识到原始异常.或者,您可以重新抛出异常并保留异常堆栈一直到原始异常:调用异常重新抛出代码消息位置您可以选择是否要传递原始代码和/或消息和/或位置.无论哪种方式,堆栈都会保留所有异常如果使用重新抛出.Rethrow 只能在 CATCH 块中使用.最后一个限制 - EXCEPTION.BAT 的完整路径不得包含 !或^.可以通过以下命令访问此文档恒定流:异常/?或异常帮助通过更多分页:异常/??OR 异常 pagedHelp可以通过以下方式访问此实用程序的版本异常/v 或异常版本EXCEPTION.BAT 由 Dave Benham 设计和编写,具有重要的DosTips 用户 jeb 和 siberia-man 的贡献.发展历程可追溯至:http://www.dostips.com/forum/viewtopic.php?f=3&t=6497
以下是用于测试 EXCEPTION.BAT 功能的脚本.该脚本递归调用自身 7 次.每次迭代都有两个 CALL,一个指向演示正常异常传播的 :label,另一个指向演示跨脚本调用的异常传播的脚本.
从递归调用返回时,如果迭代计数是 3 的倍数(迭代 3 和 6),它会抛出异常.
每个 CALL 都有自己的异常处理程序,通常报告异常,然后重新抛出修改后的异常.但如果迭代次数为 5,则处理异常并恢复正常处理.
@echo off:: 主要的setlocal enableDelayedExpansion如果未定义@Try 调用异常初始化设置/a cnt+=1echo Main Iteration %cnt% - 调用 :Sub%@尝试%(呼叫 :Subcall echo Main Iteration %cnt% - :Sub 返回 %%errorlevel%%)%@EndTry%:@抓住setlocal enableDelayedExpansion回声(echo Main Iteration %cnt% - 检测到异常:回声代码 = !exception.code!echo Message = !exception.msg!回声位置 = !exception.loc!echo 重新抛出修改后的异常回声(本地端call exception rethrow -%cnt% "Main Exception^!"%~f0":@EndCatchecho Main Iteration %cnt% - 退出退出/b %cnt%:子设置本地echo :Sub Iteration %cnt% - 开始%@尝试%如果 %cnt% lss 7 (echo :Sub Iteration %cnt% - 调用%~f0"调用%~f0"%= 显示任何非异常返回码(证明如果没有异常则保留 ERRORLEVEL)=%call echo :Sub Iteration %cnt% - testException 返回 %%errorlevel%%)%= 如果迭代次数是 3 的倍数则抛出异常 =%set/a "1/(cnt%%3)" 2>nul ||(回声抛出异常调用异常 throw -%cnt% "除以 0 异常^!"%~f0")%@EndTry%:@抓住setlocal enableDelayedExpansion回声(echo :Sub Iteration %cnt% - 检测到异常:回声代码 = !exception.code!echo Message = !exception.msg!回声位置 = !exception.loc!本地端%= 如果迭代次数是 5 的倍数,则处理异常,否则用新属性重新抛出它 =%set/a "1/(cnt%%5)" 2>nul &&(echo 重新抛出修改后的异常回声(调用异常重新抛出 -%cnt% ":Sub Exception^!"%~f0") ||(调用异常清除echo 异常处理回声():@EndCatchecho :Sub Iteration %cnt% - 退出退出/b %cnt%
-- 输出 --
主迭代 1 - 调用 :Sub:Sub 迭代 1 - 开始:Sub 迭代 1 - 调用C: est estException.bat"主要迭代 2 - 调用 :Sub:Sub 迭代 2 - 开始:Sub 迭代 2 - 调用C: est estException.bat"主要迭代 3 - 调用 :Sub:Sub 迭代 3 - 开始:Sub 迭代 3 - 调用C: est estException.bat"主要迭代 4 - 调用 :Sub:Sub 迭代 4 - 开始:Sub 迭代 4 - 调用C: est estException.bat"主要迭代 5 - 调用 :Sub:Sub 迭代 5 - 开始:Sub 迭代 5 - 调用C: est estException.bat"主要迭代 6 - 调用 :Sub:Sub 迭代 6 - 开始:Sub 迭代 6 - 调用C: est estException.bat"主要迭代 7 - 调用 :Sub:Sub 迭代 7 - 开始:Sub 迭代 7 - 退出主迭代 7 - :Sub 返回 7主要迭代 7 - 退出:Sub 迭代 6 - testException 返回 7抛出异常:Sub 迭代 6 - 检测到异常:代码 = -6消息 = 除以 0 异常!位置 = C: est estException.bat重新抛出修改后的异常主要迭代 6 - 检测到异常:代码 = -6消息 = :Sub 异常!位置 = C: est estException.bat重新抛出修改后的异常:Sub 迭代 5 - 检测到异常:代码 = -6消息 = 主要异常!位置 = C: est estException.bat异常处理:Sub 迭代 5 - 退出主迭代 5 - :Sub 返回 5主迭代 5 - 退出:Sub 迭代 4 - testException 返回 5:Sub 迭代 4 - 退出主迭代 4 - :Sub 返回 4主迭代 4 - 退出:Sub 迭代 3 - testException 返回 4抛出异常:Sub 迭代 3 - 检测到异常:代码 = -3消息 = 除以 0 异常!位置 = C: est estException.bat重新抛出修改后的异常主要迭代 3 - 检测到异常:代码 = -3消息 = :Sub 异常!位置 = C: est estException.bat重新抛出修改后的异常:Sub 迭代 2 - 检测到异常:代码 = -3消息 = 主要异常!位置 = C: est estException.bat重新抛出修改后的异常主要迭代 2 - 检测到异常:代码 = -2消息 = :Sub 异常!位置 = C: est estException.bat重新抛出修改后的异常:Sub 迭代 1 - 检测到异常:代码 = -2消息 = 主要异常!位置 = C: est estException.bat重新抛出修改后的异常主要迭代 1 - 检测到异常:代码 = -1消息 = :Sub 异常!位置 = C: est estException.bat重新抛出修改后的异常未处理的批处理异常:代码 = -1Msg = 主要异常!Loc = C: est estException.batStack= testException [-1:Main Exception!] :Sub [-1::Sub Exception!] C: est estException.bat [-2:Main Exception!] :Sub [-2::Sub Exception!] C: est estException.bat [-3:Main 异常!] :Sub [-3::Sub 异常!] [-3:除以 0 异常!]
最后,这里有一系列简单的脚本,展示了如何有效地使用异常,即使中间脚本对它们一无所知!
从一个简单的除法脚本实用程序开始,该实用程序将两个数字相除并打印结果:
divide.bat
::divide.bat 分子除数@回声关闭设置本地set/a 结果=%1/%2 2>nul ||调用异常 throw -100 "除法异常" "divide.bat"回声 %1/%2 = %result%退出/b
请注意脚本如何在检测到错误时抛出异常,但它不会捕获异常.
现在我将编写一个对批处理异常完全幼稚的划分测试工具.
testDivide.bat
@echo offfor/l %%N in (4 -1 0) do call 12 %%Necho 成功完成!
--输出--
C: est>testDivide12/4 = 312/3 = 412/2 = 612/1 = 12未处理的批处理异常:代码 = -100Msg = 除法异常loc =divide.batStack= testDivide 除法 [-100:除法异常]
请注意最终的 ECHO 是如何从未执行的,因为divide.bat 引发的异常未被处理.
最后我会写一个主脚本来调用原始的 testDivide 并正确处理异常:
master.bat
@echo off设置本地调用异常初始化%@尝试%调用 testDivide%@EndTry%:@抓住echo %exception.Msg% 检测到并处理调用异常清除:@EndCatchecho 成功完成!
-- 输出 --
C: est>master12/4 = 312/3 = 412/2 = 612/1 = 12检测和处理除法异常顺利完成!
主脚本能够成功捕捉到一个由divide.bat 引发的异常,即使它必须通过testDivide.bat,它对异常一无所知.很酷:-)
现在这肯定不是所有与错误处理相关的事情的灵丹妙药:
内置文档中详细描述了许多语法和代码布局限制.但没有什么太令人震惊的.
没有办法自动将所有错误视为异常.所有异常都必须由代码显式抛出.这可能是一件好事,因为错误报告是按惯例处理的 - 没有严格的规则.有些程序不遵循约定.例如,
HELP ValidCommand
返回 ERRORLEVEL 1,按照惯例,这意味着错误,而HELP InvalidCommand
返回 ERRORLEVEL 0,这意味着成功.此批处理异常技术无法捕获和处理致命的运行时错误.例如
GOTO :NonExistentLabel
仍然会立即终止所有批处理,没有任何机会捕获错误.
您可以在 http://www.dostips.com/forum/viewtopic.php?f=3&t=6497.任何未来的发展都会在那里发布.我可能不会更新这篇 StackOverflow 帖子.
Does Windows batch programming support exception handling? If not, is there any way to effectively emulate exception handling within batch files?
I would like to be able to "throw an exception" anywhere within a batch script, at any CALL level, and have the CALL stack popped repeatedly until it finds an active "TRY block", whereupon a "CATCH block" can handle the exception fully and carry on, or do some cleanup and continue popping the CALL stack. If the exception is never handled, then batch processing is terminated and control returns to the command line context with an error message.
There are already couple posted ways to terminate batch processing at any CALL depth, but none of those techniques allow for any structured cleanup activity that would normally be provided within other languages via exception handling.
Note: This is a case where I already know a good answer that has only recently been discovered, and I want to share the info
Windows batch scripting certainly does not have any formal exception handling - hardly surprising considering how primitive the language is. Never in my wildest dreams did I ever think effective exception handling could be hacked up.
But then some amazing discoveries were made on a Russian site concerning the behavior of an erroneous GOTO statement (I have no idea what is said, I can't read Russian). An English summary was posted at DosTips, and the behavior was further investigated.
It turns out that (GOTO) 2>NUL
behaves almost identically to EXIT /B, except concatenated commands within an already parsed block of code are still executed after the effective return, within the context of the CALLer!
Here is a short example that demonstrates most of the salient points.
@echo off
setlocal enableDelayedExpansion
set "var=Parent Value"
(
call :test
echo This and the following line are not executed
exit /b
)
:break
echo How did I get here^^!^^!^^!^^!
exit /b
:test
setlocal disableDelayedExpansion
set "var=Child Value"
(goto) 2>nul & echo var=!var! & goto :break
echo This line is not executed
:break
echo This line is not executed
-- OUTPUT --
var=Parent Value
How did I get here!!!!
This feature is totally unexpected, and incredibly powerful and useful. It has been used to:
- Create PrintHere.bat - an emulation of the 'nix here document feature
- Create a RETURN.BAT utility that any batch "function" can conveniently CALL to return any value across the ENDLOCAL barrier, with virtually no limitations. The code is a fleshed out version of jeb's original idea.
Now I can also add exception handling to the list :-)
The technique relies on a batch utility called EXCEPTION.BAT to define environment variable "macros" that are used to specify TRY/CATCH blocks, as well as to throw exceptions.
Before a TRY/CATCH block can be implemented, the macros must be defined using:
call exception init
Then TRY/CATCH blocks are defined with the following syntax:
:calledRoutine
setlocal
%@Try%
REM normal code goes here
%@EndTry%
:@Catch
REM Exception handling code goes here
:@EndCatch
Exceptions can be thrown at any time via:
call exception throw errorNumber "messageString" "locationString"
When an exception is thrown, it pops the CALL stack iteratively using (GOTO) 2>NUL
until it finds an active TRY/CATCH, whereupon it branches to the CATCH block and executes that code. A series of exception attribute variables are available to the CATCH block:
- exception.Code - The numeric exception code
- exception.Msg - The exception message string
- exception.Loc - The string describing the location where the exception was thrown
- exception.Stack - A string that traces the call stack from the CATCH block (or command line if not caught), all the way to the exception origin.
If the exception is fully handled, then the exception should be cleared via call exception clear
, and the script carries on normally. If the exception is not fully handled, then a new exception can be thrown with a brand new exception.Stack, or the old stack can be preserved with
call exception rethrow errorNumber "messageString" "locationString"
If an exception is not handled, then an "unhandled exception" message is printed, including the four exception attributes, all batch processing is terminated, and control is returned to the command line context.
Here is the code that makes all this possible - full documentation is embedded within the script and available from the command line via exception help
or exception /?
.
EXCEPTION.BAT
::EXCEPTION.BAT Version 1.4
::
:: Provides exception handling for Windows batch scripts.
::
:: Designed and written by Dave Benham, with important contributions from
:: DosTips users jeb and siberia-man
::
:: Full documentation is at the bottom of this script
::
:: History:
:: v1.4 2016-08-16 Improved detection of command line delayed expansion
:: using an original idea by jeb
:: v1.3 2015-12-12 Added paged help option via MORE
:: v1.2 2015-07-16 Use COMSPEC instead of OS to detect delayed expansion
:: v1.1 2015-07-03 Preserve ! in exception attributes when delayed expansion enabled
:: v1.0 2015-06-26 Initial versioned release with embedded documentation
::
@echo off
if "%~1" equ "/??" goto pagedHelp
if "%~1" equ "/?" goto help
if "%~1" equ "" goto help
shift /1 & goto %1
:throw errCode errMsg errLoc
set "exception.Stack="
:: Fall through to :rethrow
:rethrow errCode errMsg errLoc
setlocal disableDelayedExpansion
if not defined exception.Restart set "exception.Stack=[%~1:%~2] %exception.Stack%"
for /f "delims=" %%1 in ("%~1") do for /f "delims=" %%2 in ("%~2") do for /f "delims=" %%3 in ("%~3") do (
setlocal enableDelayedExpansion
for /l %%# in (1 1 10) do for /f "delims=" %%S in (" !exception.Stack!") do (
(goto) 2>NUL
setlocal enableDelayedExpansion
if "!!" equ "" (
endlocal
setlocal disableDelayedExpansion
call set "funcName=%%~0"
call set "batName=%%~f0"
if defined exception.Restart (set "exception.Restart=") else call set "exception.Stack=%%funcName%%%%S"
setlocal EnableDelayedExpansion
if !exception.Try! == !batName!:!funcName! (
endlocal
endlocal
set "exception.Code=%%1"
if "!!" equ "" (
call "%~f0" setDelayed
) else (
set "exception.Msg=%%2"
set "exception.Loc=%%3"
set "exception.Stack=%%S"
)
set "exception.Try="
(CALL )
goto :@Catch
)
) else (
for %%V in (Code Msg Loc Stack Try Restart) do set "exception.%%V="
if "^!^" equ "^!" (
call "%~f0" showDelayed
) else (
echo(
echo Unhandled batch exception:
echo Code = %%1
echo Msg = %%2
echo Loc = %%3
echo Stack=%%S
)
echo on
call "%~f0" Kill
)>&2
)
set exception.Restart=1
setlocal disableDelayedExpansion
call "%~f0" rethrow %1 %2 %3
)
:: Never reaches here
:init
set "@Try=call set exception.Try=%%~f0:%%~0"
set "@EndTry=set "exception.Try=" & goto :@endCatch"
:: Fall through to :clear
:clear
for %%V in (Code Msg Loc Stack Restart Try) do set "exception.%%V="
exit /b
:Kill - Cease all processing, ignoring any remaining cached commands
setlocal disableDelayedExpansion
if not exist "%temp%Kill.Yes" call :buildYes
call :CtrlC <"%temp%Kill.Yes" 1>nul 2>&1
:CtrlC
@cmd /c exit -1073741510
:buildYes - Establish a Yes file for the language used by the OS
pushd "%temp%"
set "yes="
copy nul Kill.Yes >nul
for /f "delims=(/ tokens=2" %%Y in (
'"copy /-y nul Kill.Yes <nul"'
) do if not defined yes set "yes=%%Y"
echo %yes%>Kill.Yes
popd
exit /b
:setDelayed
setLocal disableDelayedExpansion
for %%. in (.) do (
set "v2=%%2"
set "v3=%%3"
set "vS=%%S"
)
(
endlocal
set "exception.Msg=%v2:!=^!%"
set "exception.Loc=%v3:!=^!%"
set "exception.Stack=%vS:!=^!%"
)
exit /b
:showDelayed -
setLocal disableDelayedExpansion
for %%. in (.) do (
set "v2=%%2"
set "v3=%%3"
set "vS=%%S"
)
for /f "delims=" %%2 in ("%v2:!=^!%") do for /f "delims=" %%3 in ("%v3:!=^!%") do for /f "delims=" %%S in ("%vS:!=^!%") do (
endlocal
echo(
echo Unhandled batch exception:
echo Code = %%1
echo Msg = %%2
echo Loc = %%3
echo Stack=%%S
)
exit /b
:-?
:help
setlocal disableDelayedExpansion
for /f "delims=:" %%N in ('findstr /rbn ":::DOCUMENTATION:::" "%~f0"') do set "skip=%%N"
for /f "skip=%skip% tokens=1* delims=:" %%A in ('findstr /n "^" "%~f0"') do echo(%%B
exit /b
:-??
:pagedHelp
setlocal disableDelayedExpansion
for /f "delims=:" %%N in ('findstr /rbn ":::DOCUMENTATION:::" "%~f0"') do set "skip=%%N"
((for /f "skip=%skip% tokens=1* delims=:" %%A in ('findstr /n "^" "%~f0"') do @echo(%%B)|more /e) 2>nul
exit /b
:-v
:/v
:version
echo(
for /f "delims=:" %%A in ('findstr "^::EXCEPTION.BAT" "%~f0"') do echo %%A
exit /b
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:::DOCUMENTATION:::
EXCEPTION.BAT is a pure batch script utility that provides robust exception
handling within batch scripts. It enables code to be placed in TRY/CATCH blocks.
If no exception is thrown, then only code within the TRY block is executed.
If an exception is thrown, the batch CALL stack is popped repeatedly until it
reaches an active TRY block, at which point control is passed to the associated
CATCH block and normal processing resumes from that point. Code within a CATCH
block is ignored unless an exception is thrown.
An exception may be caught in a different script from where it was thrown.
If no active TRY is found after throwing an exception, then an unhandled
exception message is printed to stderr, all processing is terminated within the
current CMD shell, and control is returned to the shell command line.
TRY blocks are specified using macros. Obviously the macros must be defined
before they can be used. The TRY macros are defined using the following CALL
call exception init
Besides defining @Try and @EndTry, the init routine also explicitly clears any
residual exception that may have been left by prior processing.
A TRY/CATCH block is structured as follows:
%@Try%
REM any normal code goes here
%@EndTry%
:@Catch
REM exception handling code goes here
:@EndCatch
- Every TRY must have an associated CATCH.
- TRY/CATCH blocks cannot be nested.
- Any script or :labeled routine that uses TRY/CATCH must have at least one
SETLOCAL prior to the appearance of the first TRY.
- TRY/CATCH blocks use labels, so they should not be placed within parentheses.
It can be done, but the parentheses block is broken when control is passed to
the :@Catch or :@EndCatch label, and the code becomes difficult to interpret
and maintain.
- Any valid code can be used within a TRY or CATCH block, including CALL, GOTO,
:labels, and balanced parentheses. However, GOTO cannot be used to leave a
TRY block. GOTO can only be used within a TRY block if the label appears
within the same TRY block.
- GOTO must never transfer control from outside TRY/CATCH to within a TRY or
CATCH block.
- CALL should not be used to call a label within a TRY or CATCH block.
- CALLed routines containing TRY/CATCH must have labels that are unique within
the script. This is generally good batch programming practice anyway.
It is OK for different scripts to share :label names.
- If a script or routine recursively CALLs itself and contains TRY/CATCH, then
it must not throw an exception until after execution of the first %@Try%
Exceptions are thrown by using
call exception throw Code Message Location
where
Code = The numeric code value for the exception.
Message = A description of the exception.
Location = A string that helps identify where the exception occurred.
Any value may be used. A good generic value is "%~f0[%~0]",
which expands to the full path of the currently executing
script, followed by the currently executing routine name
within square brackets.
The Message and Location values must be quoted if they contain spaces or poison
characters like & | < >. The values must not contain additional internal quotes,
and they must not contain a caret ^.
The following variables will be defined for use by the CATCH block:
exception.Code = the Code value
exception.Msg = the Message value
exception.Loc = the Location value
exception.Stack = traces the call stack from the CATCH block (or command line
if not caught), all the way to the exception.
If the exception is not caught, then all four values are printed as part of the
"unhandled exception" message, and the exception variables are not defined.
A CATCH block should always do ONE of the following at the end:
- If the exception has been handled and processing can continue, then clear the
exception definition by using
call exception clear
Clear should never be used within a Try block.
- If the exception has not been fully handled, then a new exception should be
thrown which can be caught by a higher level CATCH. You can throw a new
exception using the normal THROW, which will clear exception.Stack and any
higher CATCH will have no awareness of the original exception.
Alternatively, you may rethrow an exception and preserve the exeption stack
all the way to the original exception:
call exception rethrow Code Message Location
It is your choice as to whether you want to pass the original Code and/or
Message and/or Location. Either way, the stack will preserve all exceptions
if rethrow is used.
Rethrow should only be used within a CATCH block.
One last restriction - the full path to EXCEPTION.BAT must not include ! or ^.
This documentation can be accessed via the following commands
constant stream: exception /? OR exception help
paged via MORE: exception /?? OR exception pagedHelp
The version of this utility can be accessed via
exception /v OR exception version
EXCEPTION.BAT was designed and written by Dave Benham, with important
contributions from DosTips users jeb and siberia-man.
Development history can be traced at:
http://www.dostips.com/forum/viewtopic.php?f=3&t=6497
Below is script to test the capabilities of EXCEPTION.BAT. The script recursively calls itself 7 times. Each iteration has two CALLs, one to a :label that demonstrates normal exception propagation, and the other to a script that demonstrates exception propagation across script CALLs.
While returning from a recursive call, it throws an exception if the iteration count is a multiple of 3 (iterations 3 and 6).
Each CALL has its own exception handler that normally reports the exception and then rethrows a modified exception. But if the iteration count is 5, then the exception is handled and normal processing resumes.
@echo off
:: Main
setlocal enableDelayedExpansion
if not defined @Try call exception init
set /a cnt+=1
echo Main Iteration %cnt% - Calling :Sub
%@Try%
(
call :Sub
call echo Main Iteration %cnt% - :Sub returned %%errorlevel%%
)
%@EndTry%
:@Catch
setlocal enableDelayedExpansion
echo(
echo Main Iteration %cnt% - Exception detected:
echo Code = !exception.code!
echo Message = !exception.msg!
echo Location = !exception.loc!
echo Rethrowing modified exception
echo(
endlocal
call exception rethrow -%cnt% "Main Exception^!" "%~f0<%~0>"
:@EndCatch
echo Main Iteration %cnt% - Exit
exit /b %cnt%
:Sub
setlocal
echo :Sub Iteration %cnt% - Start
%@Try%
if %cnt% lss 7 (
echo :Sub Iteration %cnt% - Calling "%~f0"
call "%~f0"
%= Show any non-exception return code (demonstrate ERRORLEVEL is preserved if no exception) =%
call echo :Sub Iteration %cnt% - testException returned %%errorlevel%%
)
%= Throw an exception if the iteration count is a multiple of 3 =%
set /a "1/(cnt%%3)" 2>nul || (
echo Throwing exception
call exception throw -%cnt% "Divide by 0 exception^!" "%~f0<%~0>"
)
%@EndTry%
:@Catch
setlocal enableDelayedExpansion
echo(
echo :Sub Iteration %cnt% - Exception detected:
echo Code = !exception.code!
echo Message = !exception.msg!
echo Location = !exception.loc!
endlocal
%= Handle the exception if iteration count is a multiple of 5, else rethrow it with new properties =%
set /a "1/(cnt%%5)" 2>nul && (
echo Rethrowing modified exception
echo(
call exception rethrow -%cnt% ":Sub Exception^!" "%~f0<%~0>"
) || (
call exception clear
echo Exception handled
echo(
)
:@EndCatch
echo :Sub Iteration %cnt% - Exit
exit /b %cnt%
-- OUTPUT --
Main Iteration 1 - Calling :Sub
:Sub Iteration 1 - Start
:Sub Iteration 1 - Calling "C: est estException.bat"
Main Iteration 2 - Calling :Sub
:Sub Iteration 2 - Start
:Sub Iteration 2 - Calling "C: est estException.bat"
Main Iteration 3 - Calling :Sub
:Sub Iteration 3 - Start
:Sub Iteration 3 - Calling "C: est estException.bat"
Main Iteration 4 - Calling :Sub
:Sub Iteration 4 - Start
:Sub Iteration 4 - Calling "C: est estException.bat"
Main Iteration 5 - Calling :Sub
:Sub Iteration 5 - Start
:Sub Iteration 5 - Calling "C: est estException.bat"
Main Iteration 6 - Calling :Sub
:Sub Iteration 6 - Start
:Sub Iteration 6 - Calling "C: est estException.bat"
Main Iteration 7 - Calling :Sub
:Sub Iteration 7 - Start
:Sub Iteration 7 - Exit
Main Iteration 7 - :Sub returned 7
Main Iteration 7 - Exit
:Sub Iteration 6 - testException returned 7
Throwing exception
:Sub Iteration 6 - Exception detected:
Code = -6
Message = Divide by 0 exception!
Location = C: est estException.bat<:Sub>
Rethrowing modified exception
Main Iteration 6 - Exception detected:
Code = -6
Message = :Sub Exception!
Location = C: est estException.bat<:Sub>
Rethrowing modified exception
:Sub Iteration 5 - Exception detected:
Code = -6
Message = Main Exception!
Location = C: est estException.bat<C: est estException.bat>
Exception handled
:Sub Iteration 5 - Exit
Main Iteration 5 - :Sub returned 5
Main Iteration 5 - Exit
:Sub Iteration 4 - testException returned 5
:Sub Iteration 4 - Exit
Main Iteration 4 - :Sub returned 4
Main Iteration 4 - Exit
:Sub Iteration 3 - testException returned 4
Throwing exception
:Sub Iteration 3 - Exception detected:
Code = -3
Message = Divide by 0 exception!
Location = C: est estException.bat<:Sub>
Rethrowing modified exception
Main Iteration 3 - Exception detected:
Code = -3
Message = :Sub Exception!
Location = C: est estException.bat<:Sub>
Rethrowing modified exception
:Sub Iteration 2 - Exception detected:
Code = -3
Message = Main Exception!
Location = C: est estException.bat<C: est estException.bat>
Rethrowing modified exception
Main Iteration 2 - Exception detected:
Code = -2
Message = :Sub Exception!
Location = C: est estException.bat<:Sub>
Rethrowing modified exception
:Sub Iteration 1 - Exception detected:
Code = -2
Message = Main Exception!
Location = C: est estException.bat<C: est estException.bat>
Rethrowing modified exception
Main Iteration 1 - Exception detected:
Code = -1
Message = :Sub Exception!
Location = C: est estException.bat<:Sub>
Rethrowing modified exception
Unhandled batch exception:
Code = -1
Msg = Main Exception!
Loc = C: est estException.bat<testException>
Stack= testException [-1:Main Exception!] :Sub [-1::Sub Exception!] C: est estException.bat [-2:Main Exception!] :Sub [-2::Sub Exception!] C: est estException.bat [-3:Main Exception!] :Sub [-3::Sub Exception!] [-3:Divide by 0 exception!]
Finally, here are a series of trivial scripts that show how exceptions can be used effectively even when intermediate scripts know nothing about them!
Start off with a simple division script utility that divides two numbers and prints the result:
divide.bat
:: divide.bat numerator divisor
@echo off
setlocal
set /a result=%1 / %2 2>nul || call exception throw -100 "Division exception" "divide.bat"
echo %1 / %2 = %result%
exit /b
Note how the script throws an exception if it detects an error, but it does nothing to catch the exception.
Now I'll write a divide test harness that is totally naive about batch exceptions.
testDivide.bat
@echo off
for /l %%N in (4 -1 0) do call divide 12 %%N
echo Finished successfully!
--OUTPUT--
C: est>testDivide
12 / 4 = 3
12 / 3 = 4
12 / 2 = 6
12 / 1 = 12
Unhandled batch exception:
Code = -100
Msg = Division exception
Loc = divide.bat
Stack= testDivide divide [-100:Division exception]
Note how the final ECHO never executes because the exception raised by divide.bat was not handled.
Finally I'll write a master script that calls the naive testDivide and properly handles the exception:
master.bat
@echo off
setlocal
call exception init
%@Try%
call testDivide
%@EndTry%
:@Catch
echo %exception.Msg% detected and handled
call exception clear
:@EndCatch
echo Finished Successfully!
-- OUTPUT --
C: est>master
12 / 4 = 3
12 / 3 = 4
12 / 2 = 6
12 / 1 = 12
Division exception detected and handled
Finished Successfully!
The master script was able to successfully catch an exception raised by divide.bat, even though it had to pass through testDivide.bat, which knows nothing about exceptions. Very cool :-)
Now this is certainly not a panacea for all things related to error handling:
There are a number of syntactical and code layout limitations that are fully described in the built in documentation. But nothing too egregious.
There is no way to automatically treat all errors as an exceptions. All exceptions must be explicitly thrown by code. This is probably a good thing, given that error reporting is handled by convention - there are no strict rules. Some programs do not follow the convention. For example,
HELP ValidCommand
returns ERRORLEVEL 1, which by convention implies an error, whileHELP InvalidCommand
returns ERRORLEVEL 0, which implies success.This batch exception technique cannot catch and handle fatal run-time errors. For example
GOTO :NonExistentLabel
will still immediately terminate all batch processing, without any opportunity to catch the error.
You can follow the development of EXCEPTION.BAT at http://www.dostips.com/forum/viewtopic.php?f=3&t=6497. Any future developments will be posted there. I likely will not update this StackOverflow post.
这篇关于Windows 批处理是否支持异常处理?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!