Exiting a batch file without exiting the command shell -and- batch file subroutines


Prepare your party hats: Batch File Week is almost over.

In your batch file, you may want to exit batch file processing (say, you encountered an error and want to give up), but if you use the exit command, that will exit the entire command processor. Which is probably not what you intended.

Batch file processing ends when execution reaches the end of the batch file. The trick therefore is to use the goto command to jump to a label right before the end of the file, so that execution "falls off the end".

@echo off
if "%1"=="" echo You must provide a file name.&goto end
if NOT EXIST "\\server\backup\%USERNAME%\nul" mkdir "\\server\backup\%USERNAME%"
if NOT EXIST "\\server\backup\%USERNAME%\nul" echo Unable to create output directory.&goto end
copy "%1" "\\server\backup\%USERNAME%"
:end

Here, there are two places where we abandon batch file execution. One is on an invalid parameter, and another is if the output directory couldn't be created (or if it isn't a directory at all).

The batch command interpreter provides a courtesy label to simply this technique: The special goto target goto :eof (with the colon) jumps to the end of the batch file. It's as if every batch file had a hidden goto label called :eof on the very last line.

The goto :eof trick becomes even more handy when you start playing with batch file subroutines. Okay, let's back up: Batch file subroutines?

By using the call command, a batch file can invoke another batch file and regain control after that other batch file returns. (If you forget the call, then control does not return. In other words, the default mode for batch file invocation is chain.) In other words, the call command lets you invoke another batch file as a subroutine. The command line parameters are received by the other batch file as the usual numbered parameters %1, %2, etc.

It's annoying having to put every subroutine inside its own batch file, so the command interpreter folks added a way to call a subroutine inside the same batch file. The syntax for this is call :label parameter parameter parameter. This is logically equivalent to a batch file recursively calling itself, except that execution begins at the specified label instead of the first line of the file. (It's as if a secret goto label were added to the top of the file.)

And since it is a batch file, execution of the called subroutine ends when execution falls off the end of the file. And that's where the special goto target comes in handy. At the end of your subroutine, you can jump to the end of the batch file (so that execution falls off the end) by doing a goto :eof.

In other words, goto :eof is the return statement for batch file subroutines.

Let's take it for a spin:

@echo off
call :subroutine a b c
call :subroutine d e f
goto :eof

:subroutine
echo My parameters are 1=%1, 2=%2, 3=%3
goto :eof

That final goto :eof is redundant, but it's probably a good habit to get into, like putting a break; at the end of your last case.

The subroutine technique is handy even if you don't really care about the subroutine, because stashing the arguments into the %n parameters lets you use the tilde operators to process the inbound parameter.

@echo off
call :printfilesize "C:\Program Files\Windows NT\Accessories\wordpad.exe"
goto :eof
:printfilesize
echo The size of %1 is %~z1
goto :eof

Okay, this isn't actually much of a handy trick because you can also do it without a subroutine:

@echo off
for %%i ^
in ("C:\Program Files\Windows NT\Accessories\wordpad.exe") ^
do echo The size of %%i is %%~zi

On the other hand, the subroutine trick combines well with the FOR command, since it lets you put complex content in the loop body without having to mess with delayed expansion:

@echo off
setlocal
set DISKSIZE=1474560
set CLUSTER=512
set DISKS=1
set TOTAL=0
for %%i in (*) do call :onefile "%%i"
set /a DISKS=DISKS+1
echo Total disks required: %DISKS%
endlocal
goto :eof

:onefile
set /a SIZE=((%~z1 + CLUSTER - 1) / CLUSTER) * CLUSTER

if %SIZE% GEQ %DISKSIZE% (
    echo File %1 does not fit on a floppy - skipped
    goto :eof
)

set /a TOTAL=TOTAL+SIZE
if %TOTAL% GEQ %DISKSIZE% (
    echo ---- need another disk
    set /a DISKS=DISKS+1
    set /a TOTAL=SIZE
)
echo copy %1
goto :eof

This program calculates the number of floppy disks it would take to copy the contents of the current directory without compression.

The setlocal command takes a snapshot of the environment for restoration when we perform the endlocal at the end. That will clean up our temporary variables when we're done.

The first two variables are parameters for the calculation, namely the disk capacity and the cluster size. (We're assuming that the root directory can hold all the files we may ultimately copy. Hey, this is just a demonstration, not a real program.)

The next two variables are our running total of the number of disks we've used so far, and how many bytes we've used on the last disk.

The for command iterates over all the files in the current directory. For each one, we call :onefile with the file name.

The :onefile subroutine does all the real work. First, it takes the file size %~z1 and rounds it up to the nearest cluster. It then sees if that size is larger than a floppy disk; if so, then we're doomed, so we just skip the file. Otherwise, we add the file to the current disk and see if it fits. If not, then we declare the disk full and put the file on a brand new disk.

After the loop is complete, we print the number of floppy disks we calculated.

(This algorithm erroneously reports that no files require one disk. Fixing that is left as an exercise.)

There's your quick introduction to the secret :eof label and batch file subroutines.

[Raymond is currently away; this message was pre-recorded.]

Comments (17)
  1. pete.d says:

    Or, you could just call "exit /b" instead of "exit".

    And "endlocal" is optional before exiting the batch file. It's required only if you want to get your original environment back within the batch file for some reason.

    Batch files are a great place to see lots of examples of stuff that was done wrong ages ago, but which can't be fixed due to backward compatibility issues.

    E.g. it's silly that "setlocal" isn't the default; someone trying to write a batch file that permanently affects the CLI environment would notice right away if they forgot to enable that, but a batch file not intended to do so can be in the wild for months or even years before someone notices that it's missing the all-important "setlocal".

    Don't even get me started on the differences between argument handling for batch files vs. the CRT or .NET (such as extra special characters besides spaces and quotes, like = and ,).

  2. Adam Rosenfield says:

    So "call file.bat" is like a fork()+exec(), while just "file.bat" is like a plain exec().  Oh the things that must be done for backwards compatibility…

  3. dalek says:

    I'm starting to like Batch File Week …

  4. Dan Bugglin says:

    @pete.d setlocal isn't default for legacy batch file support; you may wish for a batch file to modify existing environment variables such as the path or whatever.  Also I was going to say everything you just said, I just made a batch file which used for expansion since I realized I could use it to automate a build process of mine.

    http://pastebin.com/jnqXSEMz if you want a look

    I forgot you could do "procedures" in batch though.  I'll have to remember that.

    @Adam Nope, call file.bat will wait for file.bat to return.  The only forks happen if you launch a GUI program from a batch file (most of the time).  You can use start /wait if you want to wait on one, though.  Calling a batch file without using call traditionally results in the caller terminating as soon as the callee does; however it seems this doesn't happen anymore (just realized I made a batch file that forgot to use call and this didn't happen).

  5. Dan Bugglin says:

    Also I found an interesting oddity (bug?)… in my pastebin example above the 7zip for loop originally had the %version%_chrome_installer.exe parameter " quoted, but for did not like that and started ignoring the quotes around the 7-zip command line ("'C:Program' is not recognized as an internal or external command, operable program or batch file.", if you set sevenzip to a path with spaces in it).  The syntax seems right though.

  6. Rob K says:

    Batch programming makes my stomach hurt, and this is why I always install cygwin and use bash.

  7. frymaster says:

    @robk – any reason you don't use the other built-in options? WSH or powershell?

  8. Programmerman says:

    It does feel like the single greatest skill a batch file writer can have is complete knowledge of the FOR command and all the ways it can be abused.

  9. Stefan Kanthak says:

    EXIT and EXIT /B have another advantage over GOTO :EOF

    they can return an errorlevel.

    BUT: dont try to use IF ERRORLEVEL with negative values

    — ERRORLEVEL.CMD —

    %SystemRoot%System32Cmd.Exe /C Exit -1

    Echo ERRORLEVEL: %ERRORLEVEL%

    If ERRORLEVEL 0 (Echo OK [0 ^> -1]) Else (Echo ERROR [0 ^<= -1])

    — eof —

    @Raymond: it doesnt hurt to write always GOTO :label

  10. skSdnW says:

    A big problem with exiting batch files is that they don't set the exit/error code used by cmd.exe for && and || handling:

    test.cmd:

    @echo off

    call FailThisLine.exe

    if errorlevel 1 (echo.ErrorMessageHere&exit /b 1)

    echo Should not get here

    Then in cmd.exe run: test&&echo.OK||echo.Failed

    To fix it you can use ugly workarounds:

    @echo off

    call FailThisLine.exe

    if errorlevel 1 (echo.ErrorMessageHere&goto dieerrlvl)

    echo Should not get here

    :dieerrlvl

    %comspec% /c exit %errorlevel%

  11. pinwing says:

    Hmm, the number of floppies seem to be at least 2.

    I guess DISK should be initialized to 1?

    Also, if I have files a.txt (1 byte), b.txt (1474560-512 bytes) and c.txt (1 byte), then it may come to the conclusion that all files need on their own floppy (instead of putting a.txt and c.txt together).

  12. Skyborne says:

    @pinwing, I think we're looking at an instance of the Knapsack Problem, in which case finding the optimal packing knowing everything beforehand is NP-complete… and the batch file only looks at items streaming past one loop at a time.

  13. voo says:

    @Skyborne Think so too. So clearly the next step is dynamic programming with batch files!

  14. Joshua says:

    @Skyborne: I can reduce away this Knapsack problem easily if it were worth my while.

  15. DavidPLB says:

    (This algorithm erroneously reports that no files require one disk. Fixing that is left as an exercise.)

    change

    set DISKSIZE=1474560

    set CLUSTER=512

    set DISKS=1

    set TOTAL=0

    to

    set DISKSIZE=1474560

    set CLUSTER=512

    set DISKS=0

    set TOTAL=%DISKSIZE%

    so set the imaginary 0 disk to be full such that teh firt fil will alwasy 'not fit' and overflow to first disk.

    :D

  16. Kenn says:

    @WndSys

    You can work around this by always using CALL to run a batch file.  

    Let's say script.bat exits with EXIT /B 0 on success and EXIT /B 1 on error.  The following is not reliable when run from an interactive command prompt:

    script.bat && echo OK

    script.bat || echo failed

    However, this works:

    call script.bat && echo OK

    call script.bat || echo failed

  17. skSdnW says:

    @Kenn:

    This forces you to know when you are executing a batchfile, not really nice when you have a batch implementation of which etc

    [You can play it safe and put call in front of everything. -Raymond]

Comments are closed.