Windows 批处理脚本当然没有任何正式的异常处理 - 考虑到这种语言的原始性,这并不奇怪。在我最疯狂的梦想中,我从来没有想过有效的异常处理会被破坏。
但后来一些惊人的发现俄罗斯网站 http://forum.script-coding.com/viewtopic.php?id=9050关于错误的 GOTO 语句的行为(我不知道说什么,我看不懂俄语)。,并对行为进行了进一步调查。
事实证明(GOTO) 2>NUL
行为几乎与 EXIT /B 相同,除了已解析的代码块内的串联命令在有效返回后仍然执行, 在调用者的上下文中!
这是一个简短的示例,演示了大部分要点。
@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
- 输出 -
var=Parent Value
How did I get here!!!!
这个功能完全出乎意料,而且非常强大和有用。它已被用于:
- Create - 'nix here 文档功能的模拟
- 创建一个RETURN.BAT实用程序 http://www.dostips.com/forum/viewtopic.php?p=41929#p41929任何批处理“函数”都可以方便地调用 CALL 来跨 ENDLOCAL 屏障返回任何值,几乎没有任何限制。该代码是一个充实的版本.
现在我还可以将异常处理添加到列表中:-)
该技术依赖于名为 EXCEPTION.BAT 的批处理实用程序来定义用于指定 TRY/CATCH 块以及引发异常的环境变量“宏”。
在实现 TRY/CATCH 块之前,必须使用以下命令定义宏:
call exception init
然后使用以下语法定义 TRY/CATCH 块:
:calledRoutine
setlocal
%@Try%
REM normal code goes here
%@EndTry%
:@Catch
REM Exception handling code goes here
:@EndCatch
可以随时通过以下方式抛出异常:
call exception throw errorNumber "messageString" "locationString"
当抛出异常时,它会使用迭代方式弹出 CALL 堆栈(GOTO) 2>NUL
直到它找到一个活动的 TRY/CATCH,然后它分支到 CATCH 块并执行该代码。 CATCH 块可以使用一系列异常属性变量:
- exception.Code - 数字异常代码
- exception.Msg - 异常消息字符串
- exception.Loc - 描述抛出异常的位置的字符串
- exception.Stack - 一个字符串,用于跟踪从 CATCH 块(或命令行,如果未捕获)的调用堆栈,一直到异常源。
如果异常已完全处理,则应通过以下方式清除异常call exception clear
,并且脚本正常进行。如果异常没有完全处理,那么可以用一个全新的异常堆栈抛出一个新的异常,或者可以用保留旧的堆栈
call exception rethrow errorNumber "messageString" "locationString"
如果未处理异常,则打印“未处理的异常”消息,包括四个异常属性,终止所有批处理,并将控制权返回到命令行上下文。
这是使这一切成为可能的代码 - 完整的文档嵌入在脚本中,并且可以通过命令行获取exception help
or 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
下面是测试 EXCEPTION.BAT 功能的脚本。该脚本递归调用自身 7 次。每次迭代都有两个 CALL,一个到 :label,演示正常的异常传播,另一个到脚本,演示跨脚本 CALL 的异常传播。
从递归调用返回时,如果迭代计数是 3 的倍数(迭代 3 和 6),则会引发异常。
每个 CALL 都有自己的异常处理程序,通常会报告异常,然后重新抛出修改后的异常。但如果迭代计数为 5,则处理异常并恢复正常处理。
@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%
- 输出 -
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!]
最后,这里有一系列简单的脚本,展示了如何有效地使用异常,即使中间脚本对异常一无所知!
从一个简单的除法脚本实用程序开始,该实用程序将两个数字相除并打印结果:
除法.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
请注意,如果脚本检测到错误,它如何引发异常,但它不执行任何操作来捕获异常。
现在我将编写一个对于批处理异常完全天真的划分测试工具。
测试除法.bat
@echo off
for /l %%N in (4 -1 0) do call divide 12 %%N
echo Finished successfully!
- 输出 -
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]
请注意最终的 ECHO 永远不会执行,因为 diverge.bat 引发的异常未得到处理。
最后,我将编写一个主脚本来调用朴素的 testDivide 并正确处理异常:
大师.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!
- 输出 -
C:\test>master
12 / 4 = 3
12 / 3 = 4
12 / 2 = 6
12 / 1 = 12
Division exception detected and handled
Finished Successfully!
主脚本能够成功捕获由divide.bat引发的异常,即使它必须通过testDivide.bat,而testDivide.bat对异常一无所知。很酷 :-)
现在这当然不是解决与错误处理相关的所有问题的灵丹妙药:
内置文档中完整描述了许多语法和代码布局限制。但没有什么太令人震惊的。
没有办法自动将所有错误视为异常。所有异常都必须由代码显式引发。这可能是一件好事,因为错误报告是按惯例处理的——没有严格的规则。有些程序不遵循约定。例如,HELP ValidCommand
返回 ERRORLEVEL 1,按照惯例这意味着错误,而HELP InvalidCommand
返回 ERRORLEVEL 0,这意味着成功。
这种批处理异常技术无法捕获和处理致命的运行时错误。例如GOTO :NonExistentLabel
仍会立即终止所有批处理,而没有任何机会捕获错误。
您可以在以下网址关注Exception.BAT的开发情况:。任何未来的进展都会在那里发布。我可能不会更新这篇 StackOverflow 帖子。