Program Flow Control
Although there are very few constructs available for controlling the flow of batch programs, these few can be used imaginatively to accomplish most of the constructs familiar to users of high level languages.
We have only
IF file EXISTs
IF string equals string
IF ERRORLEVEL
(and their negations)
FOR variable in set DO
GOTO
CALL
invocation by
name or reference
and the rather strange
COMMAND /e:nnn /c
construct. These are discussed in
Intrinsic Commands
and
External Commands.
There are no procedures or
functions - no
DO
or
WHILE,
no
switch()
or
CASE
constructs - no return values from functions that don't exist.
None the less, all of those can be simulated or emulated, as can linked lists of commands (and perhaps even objects).

Procedures are rather easy, if you
don't mind a clutter of small files eating up your hard disk - simply make a
separate batch file of each procedure. Because most procedures would consume at
least one full cluster of disk space, and multiple files tend to become
separated and lost (especially when cloning a program to a new location and when
cleaning out dead wood from the HDD), it is usually preferable to make the basic
batch file recursive, in the sense that it calls itself rather than in the sense
that the internal code is reentrant (though it might be). This is accomplished
by
CALL %0.
%0
is the name of the program as given in the command that invoked the program, and
sometimes needs to be given in full filespec form - particularly when the
program changes the default directory and the batch file is not in the path.
This trivial example shows a single procedure call. The procedure batch file
follows the master one.
MASTER.BAT
@echo off
call foo
:end
FOO.BAT
@echo off
echo This is FOO
:end
This results in the sequence of commands
@echo off
call foo
echo This is FOO
:end
:end
Note that FOO.BAT is indented one column more than the master file and that the file names do not appear in the files.
The procedure does not need to begin
with
@ECHO OFF,
since echoing is already off. The two
:end
labels are unnecessary (there are there to show when each ends and also as
markers for an automatic text processing program that I use for special
formatting of these files).
When the two are combined into one file, and the simplest recursion method is used (see: Recursion in Batch Files), we get
@echo off
if not "%1"=="" goto foo
call %0 foo
goto end
:foo
echo This is FOO
:end
When invoked, this generates the sequence of commands
@echo off
if not ""=="" goto foo
call test foo
if not "foo"=="" goto foo
:foo
echo This is FOO
:end
goto end
:end
Note that the two
instances of
:end
are of the same label.

Functions are harder, since there is no real way to return a value, except in a global variable (an environment variable). However, we can tell the function which variable to return the value in by passing it the variable's name as a command line argument.
MASTER.BAT
@echo off
call foo string
echo %string%
:end
FOO.BAT
@echo off
set %1=This is FOO
:end
To see the action,
comment out the
@echo off
line (with ::) and run MASTER.BAT. The action is similar to the above
expansions.
Alternatively, we can jump to the function, passing it the name of the master file and the marker and commands to reenter the master file and have it reinvoke the master file with the return value as command line arguments, but this doesn't really have a common parallel construct in high level languages:
MASTER.BAT
@echo off
if not "%1"=="" %1 %2
foo %0 goto pass2
:pass2
echo %3 %4 %5
:end
FOO.BAT
@echo off
%1 %2 %3 This is FOO
:end
Procedures can also be handled this way, but without the return value. The primary use for that kind of reentrancy is to cause COMMAND.COM to forget about the master file so that it can be modified on the fly. A trivial example, that can be used only once for each copy of the master file is
MASTER.BAT
@echo off
if not "%1"=="" %1 %2
foo %0 goto pass2
:end
FOO.BAT
@echo off
echo :pass2 >> %0
REM use double '%' when a real '%' character is needed.
echo echo %%3 %%4 %%5 >> %0
%1 %2 %3 This is FOO
:end
Which constructs the target part of MASTER.BAT on the fly. Normally FOO.BAT would invoke some editor to edit the master file or copy an already edited one over it:
MASTER.BAT
@echo off
foo %0
:end
FOO.BAT
@echo off
copy baz.txt %1.bat > nul
%1
:end
BAZ.TXT
@echo off
echo This is BAZ
:end
Note that in that example, the master file must be invoked with just its name, no extension.

DO WHILE
and
WHILE DO
differ only in that the former executes its loop at least once, regardless of
the condition of the test variable. The difference is that the loop test is at
the beginning of a
WHILE DO
and at the end of a
DO WHILE
loop. These examples use the KPAUSED program from the discussion of
IF ERRORLEVEL.
Each also uses a
DIR c:\DOS
command to create a delay to allow pressing the "any" key to stop the loop.
@echo off
echo DO WHILE follows
dir c:\dos
echo Beginning DO WHILE loop
:do
echo Still running DO WHILE
kpaused
if not ERRORLEVEL 1 goto do
echo DO WHILE loop has ended - WHILE DO is next
pause
dir c:\dos
:while
kpaused
if errorlevel 1 goto done
echo Still running WHILE DO
goto while
:done
echo WHILE DO loop ended
:end

switch()
is implemented with a string of IF tests. There are at least three ways to
implement it: with negative tests and jumps around the case code, with positive
tests and CALLs to procedures, and with GOTOs to the variable (but there can be
no default case here). The first can be done this way (testing %1 against the
first three decimal digit characters):
@echo off
if not %1==0 goto t1
echo %%1 is 0
:t1
if not %1==1 goto t2
echo %%1 is 1
:t2
if not %1==1 goto default
echo %%1 is 2
:default
echo %%1 is not in the set 0, 1, 2
:end
The second could be done like this:
@echo off
if %1==0 call zero
if %1==1 call one
if %1==2 call two
call default
:end
ZERO.BAT
@echo off
echo %%1 is 0
:end
ONE.BAT
@echo off
echo %%1 is 1
:end
TWO.BAT
@echo off
echo %%1 is 2
:end
DEFAULT.BAT
@echo off
echo %%1 is not in the set 0, 1, 2
:end
or recursively, like this:
@echo off
if not "%1"=="" goto %1
if %1==0 call %0 zero
if %1==1 call %0 one
if %1==2 call %0 two
call %0 default
goto end
:zero
echo %%1 is 0
goto end
:one
echo %%1 is 1
goto end
:two
echo %%1 is 2
goto end
:default
echo %%1 is not in the set 0, 1, 2
:end
And the third like this:
@echo off
goto %1
:0
echo %%1 is 0
goto end
:1
echo %%1 is 1
goto end
:2
echo %%1 is 2
:end
(Note: the use of numerical names for environment variables is not always reliable.)

Something along the
lines of
FOR( i = 1; i <= n; i++ )
can be done using a bang counter ('!' is the "bang" character - though any
character can be used, '!' is sort of customary):
set n=%1
set i=
:loop
set i=%i%!
code to do something repetitive
if %i%==%n% goto end
goto loop
:end
This builds a string of bangs,
adding one on each pass, until the I string matches the N string, at which point
it exist. This is really a DO loop that loops on <= but it can be converted to a
WHILE loop that loops while
i < n
by moving the IF test to the line following the SET in the loop. In the form
given, the test will crash on the first pass if it is ahead of the SET (syntax
error because
I == nul).
A different approach to a
bang counter
is found in the section on list processing.
