Exception Handling with batch using erroneous GOTO
Posted: 09 Jun 2015 17:04
Important Update - The current version of EXCEPTION.BAT (version 1.4) is available at viewtopic.php?f=3&t=6497&p=48480#p48480.
But go ahead and read the entire thread for explanations and to get a sense of the development history.
Up until now, I've been aware of two good methods for terminating batch processing, regardless how deep the CALL stack. Both methods are described at StackOverflow: Exit batch script from inside a function:
1) Jeb's fatal syntax error method - This works, but it has a nasty side effect. Environment changes after SETLOCAL are preserved, meaning the implicit ENDLOCAL does not fire at the end of batch processing.
2) My Ctrl-C method - This works cleanly, all SETLOCAL are properly released at end.
But both methods are limited in that there is no mechanism for CALLs to catch the closure, so there is no cleanup. (no exception handling)
But the topic Tricky way to detect calls from another batch script led me to a new way of cleanly exiting all batch processing, and this method provides primitive exception handling. Each CALL has the option of specifying cleanup code to execute in the event that batch processing has been terminated from within any arbitrary deeper CALL
Note that these exception cleanup commands are executed within a command line context, after all SETLOCAL have been released. Also, there is no way to "handle" the exception and resume normal batch processing. Despite these limitations, the method is still potentially very useful.
One of the problems I had to solve was how to tell whether code is executing in a batch context, or a command line context. I have found the following simple test works great:
The above works, regardless whether the command line has delayed expansion enabled or disabled
The other problem was figuring a way for the exception handler to detect when the exception has been raised (batch processing terminated). I first attempted to return an unusual return code (similar to the Ctrl-C method), but I couldn't make that work.
I could have checked if in a command line context, but I opted not to go that route.
Instead, I simply define a _KillBatchException variable at the end of the code that raises the exception. Upon CALL return, if _KillBatchException is defined, then there was an exception, otherwise not.
Putting it all together, I define a KillBatch.bat script that is intended to be somewhere within PATH:
KillBatch.bat
Here is a test script that demonstrates how KilBatch.bat can be used. It is a recursive script that expects two arugments:
1) The total number of recursive levels
2) The level at which the exception should be raised.
The script raises the exception on the way out of the recursive call.
test.bat
Here are the results of a test run:
Sweet
Dave Benham
EDIT - I simplified detection of the command line context
But go ahead and read the entire thread for explanations and to get a sense of the development history.
Up until now, I've been aware of two good methods for terminating batch processing, regardless how deep the CALL stack. Both methods are described at StackOverflow: Exit batch script from inside a function:
1) Jeb's fatal syntax error method - This works, but it has a nasty side effect. Environment changes after SETLOCAL are preserved, meaning the implicit ENDLOCAL does not fire at the end of batch processing.
2) My Ctrl-C method - This works cleanly, all SETLOCAL are properly released at end.
But both methods are limited in that there is no mechanism for CALLs to catch the closure, so there is no cleanup. (no exception handling)
But the topic Tricky way to detect calls from another batch script led me to a new way of cleanly exiting all batch processing, and this method provides primitive exception handling. Each CALL has the option of specifying cleanup code to execute in the event that batch processing has been terminated from within any arbitrary deeper CALL
Note that these exception cleanup commands are executed within a command line context, after all SETLOCAL have been released. Also, there is no way to "handle" the exception and resume normal batch processing. Despite these limitations, the method is still potentially very useful.
One of the problems I had to solve was how to tell whether code is executing in a batch context, or a command line context. I have found the following simple test works great:
Code: Select all
(
setlocal enableDelayedExpansion
if "!!" equ "" (
endlocal
echo BATCH CONTEXT
) else echo COMMAND LINE CONTEXT
)
The above works, regardless whether the command line has delayed expansion enabled or disabled
The other problem was figuring a way for the exception handler to detect when the exception has been raised (batch processing terminated). I first attempted to return an unusual return code (similar to the Ctrl-C method), but I couldn't make that work.
I could have checked if in a command line context, but I opted not to go that route.
Instead, I simply define a _KillBatchException variable at the end of the code that raises the exception. Upon CALL return, if _KillBatchException is defined, then there was an exception, otherwise not.
Putting it all together, I define a KillBatch.bat script that is intended to be somewhere within PATH:
KillBatch.bat
Code: Select all
@echo off
(
for /l %%. in (1 1 10) do (goto)2>nul
setlocal enableDelayedExpansion
if "!!" equ "" (
endlocal
call "%~f0"
) else set "_KillBatchException=1"
)
Here is a test script that demonstrates how KilBatch.bat can be used. It is a recursive script that expects two arugments:
1) The total number of recursive levels
2) The level at which the exception should be raised.
The script raises the exception on the way out of the recursive call.
test.bat
Code: Select all
@echo off
:: Critical to undefine this variable at start of batch
set "_KillBatchException="
:: Explicitly clear these variables so we can prove all ENDLOCAL are honored
set "CommandLineContext="
set "#depth="
set "#maxDepth="
set "#killDepth="
setlocal
set "CommandLineContext=BatchContext"
set /a "#depth=0, #maxDepth=%~1+0, #killDepth=%~2+0"
:test
setlocal
set /a #depth+=1
echo Enter :test level %#depth%
if %#depth% lss %#maxDepth% (
call :test
if defined _KillBatchException call echo level %#depth% exception cleanup %%CommandLineContext%%
)
if %#depth% equ %#killDepth% (
call echo Level %#depth% exception exit %%CommandLineContext%%
call killBatch
)
call echo Level %#depth% normal exit %%CommandLineContext%%
exit /b
Here are the results of a test run:
Code: Select all
D:\test>test 20 12
Enter :test level 1
Enter :test level 2
Enter :test level 3
Enter :test level 4
Enter :test level 5
Enter :test level 6
Enter :test level 7
Enter :test level 8
Enter :test level 9
Enter :test level 10
Enter :test level 11
Enter :test level 12
Enter :test level 13
Enter :test level 14
Enter :test level 15
Enter :test level 16
Enter :test level 17
Enter :test level 18
Enter :test level 19
Enter :test level 20
Level 20 normal exit BatchContext
Level 19 normal exit BatchContext
Level 18 normal exit BatchContext
Level 17 normal exit BatchContext
Level 16 normal exit BatchContext
Level 15 normal exit BatchContext
Level 14 normal exit BatchContext
Level 13 normal exit BatchContext
Level 12 exception exit BatchContext
level 11 exception cleanup %CommandLineContext%
level 10 exception cleanup %CommandLineContext%
level 9 exception cleanup %CommandLineContext%
level 8 exception cleanup %CommandLineContext%
level 7 exception cleanup %CommandLineContext%
level 6 exception cleanup %CommandLineContext%
level 5 exception cleanup %CommandLineContext%
level 4 exception cleanup %CommandLineContext%
level 3 exception cleanup %CommandLineContext%
level 2 exception cleanup %CommandLineContext%
level 1 exception cleanup %CommandLineContext%
D:\test>set #
Environment variable # not defined
Sweet
Dave Benham
EDIT - I simplified detection of the command line context