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.


  ** Copyright 1995, Ted Davis - all rights reserved ** 

Input and feedback from readers are welcome. NOTE: the subject of the message must contain the word "batch" for the message to get past the spam filter.

Back to the Table of Contents page

Back to my personal links page - back to my home page