18
votes

I have a problem with my batch file. It builds several programs automatically by doing something like this:

  • set some compilation flags
  • run 'gmake all'
  • call the "check error level" function and if errorlevel 1, exit

So it looks like this:

set FLAG=1
...
gmake all
call :interactive_check
set OTHERFLAG=1
...
gmake all
call :interactive_check

There's 6 or 7 of these (and it might grow). So I made a function to check errorlevel instead of copy/pasting it at every step. The problem is this: the error checking is made through a function:

:interactive_check
if errorlevel 1 (
echo.
echo /!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\
echo Error in compilation process... exiting
echo /!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\
echo.
cd %root_dir%
exit /B 1
) ELSE (
echo.Continuing to next step
)
goto:eof

Now, when running it, the exit /B 1 simply exits the function, but not the batch file.

Do you know how to exit the complete batch file without having to copy/paste my "if errorlevel 1.." at every step?

6
You can't. Just replace call :interactive_check with if errorlevel 1 goto error. There is not much difference between copy-pasting former or latter. :)atzz
Got it, solved the problem with this and it works fine, thanks!Gui13

6 Answers

24
votes

For a good solution see the part improved version

You can stop a batch at any point, also inside of nested function calls.

You only need to create a syntax error, for example with an empty block (), to suppress the error message, it can be executed in a call, and the stderr of the call is redirected to nul.

@echo off
setlocal enabledelayedexpansion

rem Do something
call :interactive_check

rem Do something
call :interactive_check

goto :eof

::::::::::::::::::::::::::
:interactive_check
if errorlevel 1 (
    echo.
    echo /!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\
    echo Error in compilation process... exiting
    echo /!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\
    call :halt 1
) ELSE (
    echo.Continuing to next step
)
goto :eof

:: Sets the errorlevel and stops the batch immediately
:halt
call :__SetErrorLevel %1
call :__ErrorExit 2> nul
goto :eof

:__ErrorExit
rem Creates a syntax error, stops immediately
() 
goto :eof

:__SetErrorLevel
exit /b %time:~-2%
goto :eof

2017-04-09 Improved version: Exit only the current batch, but not the caller batch

As @dbenham mentioned, there is a new technic for exception handling that can also be used for exiting only the current batch.

@echo off
echo Do something, detecting some fatal error
call :ExitBatch 3
exit /b

:ExitBatch [errorcode] - Exits only the current batch file, regardless how many CALLs
set _errLevel=%1
REM *** Remove all calls from the current batch from the call stack
:popStack
(
    (goto) 2>nul
    setlocal DisableDelayedExpansion    
    call set "caller=%%~0"
    call set _caller=%%caller:~0,1%%
    call set _caller=%%_caller::=%%
    if not defined _caller (
        REM callType = func
        rem set _errLevel=%_errLevel%
        goto :popStack
    )   
    (goto) 2>nul
    endlocal
    cmd /c "exit /b %_errLevel%"
)
echo NEVER REACHED
exit /b
22
votes

Using a fatal syntax error, as jeb demonstrates, does kill all batch processing, but it also has a nasty side effect - environment changes after SETLOCAL are preserved, even though they are supposed to be discarded via implicit ENDLOCAL when batch processing ends. See my DosTips post SETLOCAL continues after batch termination! for more information.

Based on information at Why does a non-interactive batch script think I've pressed control-C?, I have discovered a clean way to exit all batch scripting from within a CALLed routine or script, and all changes after SETLOCAL are properly discarded.

@echo off
setlocal
set test=AFTER main SETLOCAL
call :sub
echo returning from main  NEVER REACHED
exit /b

:sub
setlocal
set test=AFTER sub SETLOCAL
set test
call :ExitBatch
echo returning from sub2 NEVER REACHED
exit /b


:ExitBatch - Cleanly exit batch processing, regardless how many CALLs
if not exist "%temp%\ExitBatchYes.txt" call :buildYes
call :CtrlC <"%temp%\ExitBatchYes.txt" 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 ExitBatchYes.txt >nul
for /f "delims=(/ tokens=2" %%Y in (
  '"copy /-y nul ExitBatchYes.txt <nul"'
) do if not defined yes set "yes=%%Y"
echo %yes%>ExitBatchYes.txt
popd
exit /b

Here is sample output of running the above test.bat. You can see that the script never returned from the :ExitBatch call, and the test variable definition has been properly discarded once batch processing terminates.

C:\test>test.bat
test=AFTER sub SETLOCAL

C:\test>set test
Environment variable test not defined

C:\test>

The :ExitBatch routine can be put into its own ExitBatch.bat script and placed somewhere within your PATH such that it can be conveniently used by any batch script.

@echo off
:ExitBatch - Cleanly exit batch processing, regardless how many CALLs
if not exist "%temp%\ExitBatchYes.txt" call :buildYes
call :CtrlC <"%temp%\ExitBatchYes.txt" 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 ExitBatchYes.txt >nul
for /f "delims=(/ tokens=2" %%Y in (
  '"copy /-y nul ExitBatchYes.txt <nul"'
) do if not defined yes set "yes=%%Y"
echo %yes%>ExitBatchYes.txt
popd
exit /b

Important Update:

It is now possible to implement robust exception handling with pure batch. Not only can you throw an exception that can terminate all batch processing from any CALL level, but you can also catch the exception at a higher level, excecute special exception handling cleanup code, and resume processing, or continue to exit via another throw! See Does Windows batch support exception handling? for more info.

5
votes

When you use exit /b X to exit from the function it sets ERRORLEVEL to the value of X. You can then use the || conditional processing symbol to execute another command if ERRORLEVEL is non-zero after the call.

@echo off
setlocal
call :myfunction PASS || goto :eof
call :myfunction FAIL || goto :eof
echo Execution never gets here
goto :eof

:myfunction
    if "%1"=="FAIL" ( 
        echo myfunction: got a FAIL. Will exit.
        exit /b 1
    )
    echo myfunction: Everything is good.
    exit /b 0

Output from this script is:

myfunction: Everything is good.
myfunction: got a FAIL. Will exit.
2
votes

I suggest following solution:

@echo off

:: comment next line if you want to export any local variables in caller environment
setlocal

set FLAG=1
rem Do something
call :interactive_check

set FLAG2=2
rem Do something
call :interactive_check

goto :eof

:interactive_check
if errorlevel 1 (
    echo.
    echo /!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\
    echo Error in compilation process... exiting
    echo /!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\
    (goto) 2>nul & endlocal & exit /b %ERRORLEVEL%
) else (
    echo Continuing to next step
)
goto :eof

It saves error code from command which produce error. If you want to save error code to specific value, then modify the line:

(goto) 2>nul & endlocal & exit /b YOUR_EXITCODE_HERE

I don't like solution with syntax error (see jeb's post), because it terminates also caller of current batch file also.

0
votes

Exceeding stack limits will terminate all batch processing immediately:

@echo off

echo I am %0, calling :subroutine
call:subroutine

NEVER PARSED
exit /b


:subroutine
echo I am %0, calling :recurse
echo Exiting batch script IMMEDIATELY

cmd /c exit /b %=EXIT CODE HERE=%
2>nul call:recurse

NEVER PARSED
exit /b


:recurse - Aborts all batch processing, regardless of CALL depth
call:recurse

NEVER PARSED
exit /b
0
votes

Most solutions only work for 2 stack levels or with on-screen error messages.
So I came up with two solutions and I think both are better than what I've seen around here.

SOLUTION FOR COMPLEX SCRIPTS WITHOUT SETLOCAL AND WITHOUT ERRORLEVEL

This solution applies when:

  • you have too many recursive or conditional functions not knowing exactly when the program will terminate
  • you don't care about variables that stay in the environment
  • you don't care about errorLevel
CALL :leave 2>nul
:leave
()
GOTO :eof

Explanation: It will call :leave putting it at the stack level, and pseudo-parameter 2>nul will send error messages to void, suppressing errors from that block of code. And then () will generate a fatal error that will kill the entire program.

You can even remove your variables by setting SET blank or putting ENDLOCAL in the main before CALL :leave. But in a complex program you wouldn't know when to do this. But in this case, I came up with a better solution...

I know many have seen this code around here: (GOTO) 2>nul & ENDLOCAL & EXIT /b %yourCode%, but like I said, it just kills the current function and terminates the previous function that called it. It doesn't work if you have multiple functions on the stack calling each other. So you could force several errors:

(GOTO) 2>nul & (GOTO) 2>nul & (GOTO) 2>nul & ENDLOCAL & EXIT /b

However, you don't know how many times to do this, and if you overdo it, you can even kill the :main and execute an exit /b that will close the console. Then I have the

SOLUTION FOR COMPLEX SCRIPTS WITH SETLOCAL AND WITH ERRORLEVEL

This solution applies when:

  • you have too many recursive or conditional functions not knowing exactly when the program will terminate
  • you don't want remnant variables
  • you check errorLevel (optional)
SET dummy=iAmDefined
FOR /l %%i in (0, 1, 1000) do (
  IF defined dummy (
    (GOTO) 2>nul & IF NOT defined dummy (CMD /c EXIT /b %yourExitCode%)
  )
)

It won't run a thousand times, but only as many times as necessary until !dummy! miss the assignment. You don't need to enable EnableDelayedExpansion unless you want to debug the !dummy! echo.

Example:

@ECHO off
SETLOCAL EnableDelayedExpansion
SET dummy=iAmDefined

CALL :func3

:func1
ECHO i'm in func1
CALL :leave 2>nul
::GOTO :eof

:func2
ECHO i'm in func2
CALL: func1
::GOTO :eof

:func3
ECHO i'm in func3
CALL: func2
::GOTO :eof

:leave
ECHO ===leaving===
FOR /l %%i in (1, 1, 1000) of (
  IF defined dummy (
    (GOTO) & ECHO %%i !dummy! & IF NOT defined dummy (CMD /c EXIT /b 555)
  )
)
::GOTO :eof
::ENDLOCAL

Exit e Explanation:

             -- 1 call  (called func3)
i'm in func3 -- 2 calls (called func2)
i'm in func2 -- 3 calls (called func1)
i'm in func1 -- 4 calls (called leave)
===leaving===
1 iAmDefined -- rollback :leave
2 iAmDefined -- rollback :func3
3 iAmDefined -- rollback :func2
4 iAmDefined -- rollback :func1
5 !dummy!    -- rollback :main before unset dummy
                new CMD and exit after unset dummy

At the 5 dummy it was still set before the (GOTO), but the ECHO was after. The :EOF are commented to ilustrate that these gotoes are not used