本文介绍了Windows批处理是否支持异常处理?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

Windows批处理编程是否支持异常处理?如果没有,有没有办法有效地模拟批处理文件中的异常处理?



我想能够在批处理脚本中的任何地方抛出异常任何CALL级别,并且CALL堆栈重复弹出,直到找到一个活动的TRY块,此时CATCH块可以完全处理异常并进行一些清理,并继续弹出CALL堆栈。如果从未处理异常,则批处理将被终止,控制返回到命令行上下文并显示一条错误消息。



已经有终止批处理,但是这些技术都不允许任何通常通过异常提供其他语言的结构化清理活动



注意: 这是一个我已经知道只有一个很好的答案最近被发现,我想分享信息

解决方案

Windows批处理脚本当然没有任何正式的异常处理 - 考虑到这种语言的原始原理并不奇怪。从来没有在我最疯狂的梦想中,我曾经认为有效的异常处理可能被黑客入侵。



但是,一些惊人的发现是在一个关于错误的GOTO声明的行为(我不知道该说什么,我不能读俄语)。 ,行为进一步调查。



事实证明,(GOTO)2> NUL 的行为几乎相当于EXIT / B,

这是一个简短的例子,演示了大部分的重点。

  @echo off 
setlocal enableDelayedExpansion
设置var =父值

调用:test
echo此行和以下行不执行
exit / b

:break
echo我是如何得到这里^^!^^!^^!^^!
exit / b

:test
setlocal disableDelayedExpansion
setvar = Child Value
(goto)2> nul& echo var =!var! &安培; goto:break
echo此行不执行

:break
echo此行不执行

- OUTPUT -

  var =父值
如何我到了这里!!!

此功能完全意外,功能强大而有用。它已被用于:




  • 创建 - 这里的nix文档功能的仿真

  • 创建一个,任何批处理函数可以方便地呼叫返回任何值ENDLOCAL屏障,几乎没有限制。该代码是的丰富版本。



现在我还可以在列表中添加异常处理: - )



该技术依赖于一个名为EXCEPTION.BAT的批处理实用程序来定义用于指定TRY / CATCH块的环境变量宏,以及抛出异常。



在实现TRY / CATCH块之前,必须使用以下方式定义宏:

 调用异常init 

然后TRY / CATCH块使用以下语法定义:

 :calledRoutine 
setlocal
%@尝试%
REM正常代码到这里
%@ EndTry%
:@Catch
REM异常处理代码到这里
:@EndCatch

异常可以抛出在任何时间通过:

  call exception throw errorNumbermessa geStringlocationString

当抛出异常时,它会使用(GOTO)2> NUL 直到找到活动的TRY / CATCH,然后分支到CATCH块并执行该代码。一系列异常属性变量可用于CATCH块:




  • exception.Code - 数字异常代码

  • exception.Msg - 异常消息字符串

  • exception.Loc - 描述引发异常的位置的字符串

  • 异常。堆栈 - 从CATCH块(或命令行(如果未捕获))跟踪调用堆栈的字符串,一直到异常源。



如果异常被完全处理,则应该通过调用异常清除来清除异常,并且脚本正常进行。如果异常没有被完全处理,那么一个新的异常可以抛出一个全新的异常。堆栈或旧的堆栈可以用

  call exception rethrow errorNumbermessageStringlocationString

如果未处理异常,然后打印一个未处理的异常消息,包括四个异常属性,所有批处理都将终止,控制返回到命令行上下文。



将所有这些可能的完整文档的代码嵌入到脚本中,并通过异常帮助 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而不是操作系统来检测延迟扩展
:: v1.1 2015-07-03保存!延迟扩展启用时的异常属性
:: v1.0 2015-06-26带嵌入文档的初始版本发行版
::
@echo off
如果%〜1 equ/ ?? goto pagedHelp
如果%〜1equ/? goto help
如果%〜1equgoto help
shift / 1& goto%1


:throw errCode errMsg errLoc
setexception.Stack =
::到达:rethrow


:rethrow errCode errMsg errLoc
setlocal disableDelayedExpansion
如果未定义exception.Restart设置exception.Stack = [%〜1:%〜2]%exception.Stack%
for / fdelims =%% 1 in(%〜1)为/ fdelims =%% 2 in(%〜2)为/ fdelims =%% 3 in %〜3)do(
setlocal enableDelayedExpansion
for / l %%#in(1 1 10)do for / fdelims =%% S in(!exception.Stack! )do(
(goto)2> NUL
setlocal enableDelayedExpansion
如果!!equ(
endlocal
setlocal disableDelayedExpansion
call set funcName = %%〜0
调用setbatName = %%〜f0
如果已定义exception.Restart(设置exception.Restart =)else调用setexception.Stack = %% funcName %%%% S
setlocal EnableDelayedExpansion
if!exception.Try!==!batName!:!funcName!(
endlocal
endlocal
setexception.Code = %% 1
if!!equ(
call%〜f0setDelayed
)else(
设置exception.Msg = %% 2
setexception.Loc = %% 3
setexception.Stack = %% S

setexception .Try =
(CALL)
goto:@Catch

)else(
for %% V in(Code Msg Loc Stack Try Restart)do set %% V =
如果^!^equ^!(
调用%〜f0showDelayed
)else(
echo(
echo未处理的批处理异常:
echo代码= %% 1
echo Msg = %% 2
echo Loc = %% 3
echo Stack = %% S

echo on
call%〜f0Kill
)>& 2

set exception.Restart = 1
setlocal disableDelayedExpansion
调用%〜f0rethrow%1%2%3

::从不到达


:init
set@ Try = call set exception.Try = %%〜f0:%%〜0
set@ EndTry = setexception.Try =& goto:@endCatch
::到达:清除


:清除
中的%% V(代码消息位置堆栈重新启动尝试)做设置例外:%% V =
退出/ b


:杀死 - 停止所有处理,忽略任何剩余的缓存命令
setlocal disableDelayedExpansion
如果没有存在%temp%\Kill.Yes调用:buildYes
调用:CtrlC<%temp%\Kill.Yes1> nul 2>& 1
:CtrlC
@cmd / c exit -1073741510

:buildYes - 为OS使用的语言建立一个Yes文件
pushd%temp%
setyes =
copy nul Kill.Yes> nul
for / fdelims =(/ tokens = 2%% Y in(
'copy / -y nul Kill.Yes< nul) '
)如果没有定义,请设置是yes = %% Y
echo%yes%> Kill.Yes
popd
exit / b


:setDelayed
setLocal disableDelayedExpansion
for %%。in(。)do(
setv2 = %% 2
setv3 = %% 3
setvS = %% S


endlocal
setexception.Msg =%v2:!= ^!%
设置 exception.Loc =%v3:!= ^!%
setexception.Stack =%vS:!= ^!%

exit / b


:showDelayed -
setLocal disableDelayedExpansion
for %%。 in(。)do(
setv2 = %% 2
setv3 = %% 3
setvS = %% S

for / fdelims =%% 2 in(%v2:!= ^!%)为/ fdelims =%% 3 in(%v3:!= ^!%) / fdelims =%% S in(%vS:!= ^!%)do(
endlocal
echo(
echo未处理批处理异常:
echo代码= %% 1
echo Msg = %% 2
echo Loc = %% 3
echo Stack = %% S

exit / b


:help
setlocal disableDelayedExpansion
for / fdelims =:%% N in('findstr / rbn::: DOCUMENTATION: ::%〜f0')设置skip = %% N
for / fskip =%skip%tokens = 1 * delims =:%% A in('findstr / n ^%〜f0')做回声(%% B
退出/ b


: - ??
:pagedHelp
setlocal disableDelayedExpansion
/ fdelims =:%% N in('findstr / rbn::: DOCUMENTATION :::%〜f0')设置skip = %% N
((for / fskip =%skip%tokens = 1 * delims =:%% A in('findstr / n^%〜f0')do @echo(%% B)| more / e )2> nul
退出/ b


:-v
:/ v
:version
echo(
for / fdelims =:%% A in('findstr^ :: EXCEPTION.BAT%〜f0') echo %% A
退出/ b


:::::::::::::::::::::::::::: ::::::::::::::::::::::::::::::::::::::::::::::::::
::: DOCUMENTATION :::

EXCEPTION.BAT是一个纯批处理脚本实用程序,可在批处理脚本中提供强大的异常
处理。它可以将代码放置在TRY / CATCH块中。
如果不抛出异常,则只执行TRY块内的代码。
如果抛出异常,批量CALL堆栈将重复弹出,直到
到达活动的TRY块,此时控制权被传递到关联的
CATCH块,并且正常处理从该恢复点。 CATCH
块中的代码将被忽略,除非抛出异常。

一个异常可能会被捕获在不同的脚本中。

如果在抛出异常之后没有找到活动的TRY,则将未处理的
异常消息打印到stderr,所有处理在
当前CMD shell中终止,并且控件为返回到shell命令行。

使用宏指定TRY块。显然,宏必须定义
才能使用。使用以下CALL定义TRY宏

调用异常init

除了定义@Try和@EndTry之外,init例程还明确地清除任何
的剩余异常可能已经被先前处理了。

TRY / CATCH块的结构如下:

%@ Try%
REM任何正常代码都在这里
%@ EndTry%
:@Catch
REM异常处理代码到这里
:@EndCatch

- 每个TRY必须有一个关联的CATCH。

- TRY / CATCH块不能嵌套。

- 使用TRY / CATCH的任何脚本或标记例程必须在出现第一个TRY之前至少有一个
SETLOCAL。

- TRY / CATCH阻止使用标签,因此不应将其放置在括号内。
可以完成,但是当控件传递给
the:@Catch或:@EndCatch标签时,括号块被破坏,代码变得难以解释
并维护。

- 任何有效的代码可以在TRY或CATCH块中使用,包括CALL,GOTO,
:标签和平衡括号。但是,GOTO不能使用
TRY块。如果标签在同一TRY块内出现
,则只能在TRY块内使用GOTO。

- GOTO绝对不能将TRY / CATCH外部的控制转移到TRY或
CATCH块内。

- 不应该使用CALL调用TRY或CATCH块中的标签。

- 包含TRY / CATCH的CALLed例程必须在
脚本中具有唯一的标签。这通常是批量编程的好习惯。
不同的脚本可以共享:标签名称。

- 如果脚本或例程递归CALL自身并包含TRY / CATCH,则
它不能抛出异常,直到执行第一个%@ Try%

使用

抛出异常抛出代码消息位置

其中

代码=异常的数字代码值。

Message =异常的描述。

位置=一个有助于识别发生异常的字符串。
可以使用任何值。一个很好的通用值是%〜f0 [%〜0],
扩展到当前正在执行的
脚本的完整路径,后跟当前执行的例程名称
在方括号内。

如果消息和位置值包含空格或毒药
字符(如& amp; | <取代。值不能包含额外的内部引号
,它们不能包含插入符号^。

以下变量将被定义为由CATCH块使用:

exception.Code =代码值
exception.Msg =消息值
exception.Loc =位置值
exception.Stack =跟踪来自CATCH块的调用堆栈(或命令行
如果未捕获),一直到异常。

如果异常未被捕获,则所有四个值都作为
未处理的异常消息的一部分打印,并且未定义异常变量。

CATCH块应该总是在最后执行以下操作之一:

- 如果异常已被处理并且处理可以继续,则清除
异常定义通过使用

调用异常清除

清除不应该在Try块中使用。

- 如果异常没有被完全处理,那么一个新的异常应该是
抛出,可以被更高级的CATCH捕获。您可以使用正常的THROW抛出一个新的
异常,这将清除异常.Stack和任何
更高的CATCH将不会对原始异常的意识。

或者,您可以重新启动一个例外,并将exbtion栈
一直保留到原始异常:

调用异常重发代码消息位置

您可以选择是否要传递原始代码和/或
消息和/或位置。无论哪种方式,堆栈将保留所有异常
如果使用rethrow。

只能在CATCH块内使用Rethrow。


最后一个限制 - EXCEPTION.BAT的完整路径不能包含!或^。


此文档可以通过以下命令访问

常量流:异常/? OR异常帮助
通过MORE:exception /? OR异常pagedHelp

此实用程序的版本可以通过

异常/ v OR异常版本


EXCEPTION.BAT由Dave Benham设计和编写,DosTips用户jeb和西伯利亚人的重要
贡献。

开发历史可追溯到:
http://www.dostips.com/forum/viewtopic.php?f=3&t=6497

以下是测试EXCEPTION.BAT功能的脚本。脚本递归调用7次。每次迭代都有两个CALL,一个到a:标签,显示正常的异常传播,另一个到演示脚本CALL的异常传播的脚本。



递归调用,如果迭代次数是3(迭代3和6)的倍数,它会抛出异常。



每个CALL都有自己的异常处理程序,通常会报告异常然后重新抛出修改的异常。但是如果迭代次数为5,则处理异常并恢复正常处理。

  @echo off 

:: Main
setlocal enableDelayedExpansion
如果没有定义@Try调用异常init

set / a cnt + = 1
echo主迭代%cnt% - 调用:Sub
%@ Try%

call:Sub
call echo Main Iteration%cnt% - :Sub returned %% errorlevel %%

%@ EndTry%
:@Catch
setlocal enableDelayedExpansion
echo(
echo Main Iteration%cnt% - 检测到异常:
echo Code =!exception.code!
echo Message =!exception.msg!
echo Location =!exception.loc!
echo Rethrowing modified exception
echo(
endlocal
call exception rethrow - %cnt%主异常^!%〜f0 :@EndCatch
echo主迭代%cnt% - 退出
退出/ b%cnt%


:Sub
setlocal
echo:Sub迭代%cnt% - 开始
%@尝试%
如果%cnt%lss 7 (
echo:Sub Iteration%cnt% - 调用%〜f0
调用%〜f0
%=显示任何非异常返回码(如果没有异常,则会显示ERRORLEVEL )=%
调用echo:Sub迭代%cnt% - testException返回%% errorlevel %%

%=如果迭代计数是3 =%$ b的倍数,则抛出异常$ b set / a1 /(cnt %% 3)2> nul || (
echo抛出异常
调用异常throw - %cnt%除以0异常^!%〜f0<%〜0>

%@ EndTry%
:@Catch
setlocal enableDelayedExpansion
echo(
echo:Sub Iteration%cnt% - 检测到异常:
echo代码=!exception.code!
echo Message =!exception.msg!
echo Location =!exception.loc!
endlocal
%=如果迭代计数是5的倍数,则处理异常,否则使用新属性重新抛出= %
set / a1 /(cnt %% 5)2> nul&&(
echo Rethrowing modified exception
echo(
call exception rethrow - %cnt %:Sub Exception ^!%〜f0<%〜0>
)||(
调用异常清除
echo异常处理
echo(

:@EndCatch
echo:Sub迭代%cnt% - 退出
退出/ b%cnt%

- OUTPUT -

 主迭代1  - 调用:Sub 
:子迭代1 - 星t
:子迭代1 - 调用C:\test\testException.bat
主迭代2 - 调用:Sub
:子迭代2 - 开始
:Sub迭代2 - 调用C:\test\testException.bat
主迭代3 - 调用:Sub
:Sub迭代3 - 开始
:子迭代3 - 调用C: \test\testException.bat
主迭代4 - 调用:Sub
:子迭代4 - 开始
:子迭代4 - 调用C:\test\testException蝙蝠
主迭代5 - 调用:Sub
:子迭代5 - 开始
:子迭代5 - 调用C:\test\testException.bat
主迭代6 - 调用:Sub
:子迭代6 - 开始
:子迭代6 - 调用C:\test\testException.bat
主迭代7 - 调用:Sub
:子迭代7 - 开始
:子迭代7 - 退出
主迭代7 - :Sub返回7
主迭代7 - 退出
:子迭代6 - testException返回7
投掷异常

:Sub Iteration 6 - 检测到异常:
代码= -6
消息=除以0异常!
位置= C:\test\testException.bat<:Sub>
返回修改的异常


主迭代6 - 检测到异常:
代码= -6
消息=:Sub异常!
位置= C:\test\testException.bat<:Sub>
返回修改的异常


:子迭代5 - 检测到异常:
代码= -6
消息=主异常!
位置= C:\test\testException.bat< C:\test\testException.bat>
异常处理

:子迭代5 - 退出
主迭代5 - :子返回5
主迭代5 - 退出
:子迭代4 - testException返回5
:Sub迭代4 - 退出
主迭代4 - :Sub返回4
主迭代4 - 退出
:子迭代3 - testException返回4
抛出异常

:Sub迭代3 - 检测到异常:
代码= -3
消息=除以0异常!
位置= C:\test\testException.bat<:Sub>
返回修改的异常


主迭代3 - 检测到异常:
代码= -3
消息=:Sub异常!
位置= C:\test\testException.bat<:Sub>
返回修改的例外


:Sub迭代2 - 检测到异常:
代码= -3
消息=主异常!
位置= C:\test\testException.bat< C:\test\testException.bat>
返回修改后的异常


主迭代2 - 检测到异常:
代码= -2
消息=:Sub异常!
位置= C:\test\testException.bat<:Sub>
返回修改的例外


:子迭代1 - 检测到异常:
代码= -2
消息=主异常!
位置= C:\test\testException.bat< C:\test\testException.bat>
返回修改的例外


主迭代1 - 检测到异常:
代码= -1
消息=:Sub异常!
位置= C:\test\testException.bat<:Sub>
返回修改的异常


未处理的批处理异常:
代码= -1
Msg =主异常!
Loc = C:\test\testException.bat< testException>
Stack = testException [-1:Main Exception!]:Sub [-1 :: Sub Exception!] C:\test\testException.bat [-2:Main Exception!]:Sub [-2: :Sub Exception!] C:\test\testException.bat [-3:Main Exception!]:Sub [-3 :: Sub Exception!] [-3:除以0异常!]

最后,这里是一系列简单的脚本,显示了即使中间脚本对它们没有任何知识也能有效地使用异常! / p>

开始使用一个简单的分区脚本实用程序,分割两个数字并打印结果:



.bat

  :: divide.bat分子除数
@echo off
setlocal
set / a result =%1 /%2 2> nul || call exception throw -100Division exceptiondivide.bat
echo%1 /%2 =%result%
exit / b

请注意,如果脚本检测到错误,脚本会引发异常,但是没有任何可以捕获的异常。



现在我将写一个关于批处理例外的完全天真的分割测试工具。



testDivide.bat

  @echo off 
for / l %% N(4 -1 0)do call divide 12 %% N
echo已完成成功!

- OUTPUT -

  C:\test> testDivide 
12/4 = 3
12/3 = 4
12/2 = 6
12/1 = 12

未处理的批量异常:
代码= -100
Msg =分区异常
Loc = divide.bat
Stack = testDivide divide [-100:分区异常]

请注意,由于没有处理由divide.bat引发的异常,最终的ECHO将不会执行



最后,我将编写一个主脚本,调用天真的testDivide并正确处理异常:



master.bat

  @echo off 
setlocal
调用异常init

%@尝试%
调用testDivide
%@ EndTry%
:@Catch
echo%exception.Msg%检测并处理
调用异常清除
:@EndCatch
echo成功完成!

- OUTPUT -

  C:\test> master 
12/4 = 3
12/3 = 4
12/2 = 6
12/1 = 12
检测和处理部门异常
成功完成!

主脚本能够成功捕获由divide.bat引发的异常,即使它必须通过testDivide.bat,它不了解异常。非常酷: - )



现在,这并不是所有与错误处理有关的事情的灵丹妙药:




  • 有许多语法和代码布局限制已在内置文档中完整描述。但没有什么太糟糕。


  • 没有办法自动将所有错误视为例外。所有异常必须由代码显式抛出。这可能是一件好事,因为错误报告是按惯例处理的 - 没有严格的规则。一些程序不符合惯例。例如, HELP ValidCommand 返回ERRORLEVEL 1,这通常意味着错误,而 HELP InvalidCommand 返回ERRORLEVEL 0,其中意味着成功。


  • 此批处理异常技术无法捕获并处理致命的运行时错误。例如 GOTO:NonExistentLabel 仍将立即终止所有批处理,没有任何机会抓住错误。




您可以按照。任何未来的发展将在那里张贴。我可能不会更新此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:\test\testException.bat"
Main Iteration 2 - Calling :Sub
:Sub Iteration 2 - Start
:Sub Iteration 2 - Calling "C:\test\testException.bat"
Main Iteration 3 - Calling :Sub
:Sub Iteration 3 - Start
:Sub Iteration 3 - Calling "C:\test\testException.bat"
Main Iteration 4 - Calling :Sub
:Sub Iteration 4 - Start
:Sub Iteration 4 - Calling "C:\test\testException.bat"
Main Iteration 5 - Calling :Sub
:Sub Iteration 5 - Start
:Sub Iteration 5 - Calling "C:\test\testException.bat"
Main Iteration 6 - Calling :Sub
:Sub Iteration 6 - Start
:Sub Iteration 6 - Calling "C:\test\testException.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:\test\testException.bat<:Sub>
Rethrowing modified exception


Main Iteration 6 - Exception detected:
  Code     = -6
  Message  = :Sub Exception!
  Location = C:\test\testException.bat<:Sub>
Rethrowing modified exception


:Sub Iteration 5 - Exception detected:
  Code     = -6
  Message  = Main Exception!
  Location = C:\test\testException.bat<C:\test\testException.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:\test\testException.bat<:Sub>
Rethrowing modified exception


Main Iteration 3 - Exception detected:
  Code     = -3
  Message  = :Sub Exception!
  Location = C:\test\testException.bat<:Sub>
Rethrowing modified exception


:Sub Iteration 2 - Exception detected:
  Code     = -3
  Message  = Main Exception!
  Location = C:\test\testException.bat<C:\test\testException.bat>
Rethrowing modified exception


Main Iteration 2 - Exception detected:
  Code     = -2
  Message  = :Sub Exception!
  Location = C:\test\testException.bat<:Sub>
Rethrowing modified exception


:Sub Iteration 1 - Exception detected:
  Code     = -2
  Message  = Main Exception!
  Location = C:\test\testException.bat<C:\test\testException.bat>
Rethrowing modified exception


Main Iteration 1 - Exception detected:
  Code     = -1
  Message  = :Sub Exception!
  Location = C:\test\testException.bat<:Sub>
Rethrowing modified exception


Unhandled batch exception:
  Code = -1
  Msg  = Main Exception!
  Loc  = C:\test\testException.bat<testException>
  Stack= testException [-1:Main Exception!]  :Sub [-1::Sub Exception!]  C:\test\testException.bat [-2:Main Exception!]  :Sub [-2::Sub Exception!]  C:\test\testException.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:\test>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:\test>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, while HELP 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批处理是否支持异常处理?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

09-05 12:34