BATCH FILE TIPS AND UTILITIES The Seventh in a series by Mitchell A. Hoselton CALLS AND JUMPS We'll start this article by looking at ways to move the point of execution from one batch file to another. There are two approaches; batch file jumps and subroutine calls. In a batch file jump the execution pointer moves from one batch file to another. It cannot store information about where it was in the parent batch file so it has no way of returning to that point. Following a subroutine call, DOS stores the execution pointer in a subroutine stack. The subroutine stack can hold many execution pointers. The stack uses a last-in/first-out storage scheme. DOS creates a new execution pointer for each successive subroutine. When a subroutine finishes, the most recently saved execution pointer is retrieved from the subroutine stack. It still contains its last address and COMMAND.COM restarts executing the calling batch file at that point. I've already demonstrated one advantage of using subroutine calls; the FOR IN DO command can execute multiple subroutine calls. A FOR IN DO command can only execute one batch file jump, however. The FOR IN DO does not create pending jumps the way it created pending GOTOs. It jumps at the first opportunity and does not return. To further complicate matters, there are actually two types of subroutine calls. One type uses the CALL command to initiate execution of the subroutine. The other uses COMMAND.COM to initiate execution of the subroutine. These two types of subroutine calls have distinct uses. The appropriate use of the words "subroutine" and "call" applies only when storing an execution pointer in the subroutine stack. This is in contrast to a jump, which does not save the execution pointer. The linguistic style is to "call a subroutine" or to "execute a jump," whichever is appropriate. The batch file that contains the calling command for the subroutine is the "calling batch file." The batch file that contains the jumping command is often the "parent batch file." (Some use "jump" and "branch" interchangeably. I want to avoid confusing jump and the branch initiated by a GOTO command). REVIEW OF COMMAND.COM Before tackling the intricacies of batch file jumping and subroutine calling, it will be helpful to review the role that COMMAND.COM plays in executing batch files. COMMAND.COM resides in memory in two pieces; the resident and transient parts. The resident part is always in memory. Including the /p switch in the SHELL command in CONFIG.SYS causes it to load the primary command processor. If CONFIG.SYS does not contain a SHELL command to load the primary command processor, SYSINIT tells MSDOS.SYS to load the primary command processor after it finishes processing all the commands in CONFIG.SYS. The resident part of the primary command processor is permanent and resides in the low address end of low memory at all times. The transient part of COMMAND.COM resides in the high-address end of low memory. It can be overwritten by application programs that need the space it occupies. Whenever COMMAND.COM reacquires control after an application exits to DOS, the first job of the resident part of COMMAND.COM is to make sure that the transient part is still in memory. If it is, fine. If it is not, the resident part loads a new copy of the transient part into memory from the file specified by the COMSPEC environment variable. DOS can thus be sure a complete copy of COMMAND.COM is in memory and ready to execute the next command. In addition to the primary command processor, it is possible to load one or more additional copies of the resident part of COMMAND.COM. These are the secondary command processors. The command processor loaded highest in memory is the active command processor. These secondary copies are not permanent. In DOS 5.0 with DOS in the HMA each copy of the resident part of COMMAND.COM consumes 2368 bytes plus the size of the DOS environment plus a variable sized data area. When DOS loads into low memory the resident part of COMMAND.COM takes up a little more room. The whole thing takes up something like 3K to 3.5K of memory. No matter how many copies of the resident part are in memory, there is never more than one copy of the transient part of COMMAND.COM in the high address end of low memory. There is still only one primary command processor. There is only one active command processor at any given time. There are only two ways to load a secondary command processor. The type of secondary command processor that I call a temporary command processor only executes one command. That command can be a batch file. It automatically unloads as soon as the command finishes executing. If the command is a batch file, the temporary command processor unloads as soon as the last line of any batch file executes or as soon as an EXIT command executes. Users can also load a secondary command processor manually at the DOS prompt. A temporary command processor, loaded solely for the purpose of running a batch file subroutine, will unload itself from memory under several different circumstances. The simplest way is to let the batch file subroutine terminate normally by executing its last line. That signals the end of the subroutine and time for the temporary command processor to unload itself. Alternatively, issuing an EXIT command forces an active secondary command processor to unload itself, immediately. Performing any number of batch file jumps does not tell the temporary command processor to unload from memory. The temporary command processor will unload as soon as any one of the target batch files ends, however. Manually loaded secondary command processors unload themselves from memory only in response to an EXIT command issued either manually or in a batch file. This is a good way to protect the primary DOS environment while playing with environment variables. Try this out; switch to the drive and directory containing COMMAND.COM and type the following command at the DOS prompt. COMMAND Type it several times. Hit the [RETURN] key each time. Each time you enter this command it loads another secondary command processor. Each command processor gets its own copy of the DOS environment variables. The secondary command processor loaded highest in memory is the active command processor. Use the MEM command in DOS 5.0 to watch the available memory decrease by about 3K after loading each command processor into memory. To view the current DOS environment, issue a SET command. Then execute the following command several times. Specify a different environment variable name each time. Do not use the COMSPEC variable. SET variable_name= This will delete several DOS environment variables. Issue a second SET command to be sure that the DOS variable(s) you named was (were) deleted. This process only erases variables from the current copy of the DOS environment. Manually issue an EXIT command. That unloads one secondary command processor and its copy of the DOS environment. Issue another SET command. The current DOS environment contains all the variables, including the variables deleted from the copy. Every time you or your batch file issues an EXIT command, one secondary command processor unloads from memory. Issue enough EXIT commands to unload all the secondary command processors. Use the MEM command to watch the available memory increase by about 3K each time a secondary command processor unloads itself from memory. The EXIT command never removes the primary command processor. The changes made to a copy of the DOS environment do not effect the primary DOS environment owned by the primary command processor. Whenever a secondary command processor loads into memory, it fetches a copy of the DOS environment from the processor loaded immediately below it in memory. Changes to the DOS environment can be passed up a chain of successively loaded secondary command processors. Those changes cannot be passed back down the chain as the secondary command processors are removed from memory, however. BATCH FILE JUMPING When the execution pointer jumps from one batch file to another, there is a simple analogy with using the GOTO command to branch within a batch file. In both cases DOS keeps no record of where the branch or jump started and has no inherent way of returning to the branch or jump point. Jumping from one batch file to another is conceptually simpler than using subroutines, so we'll cover it first. Whether calling a subroutine or jumping to a batch file, the new file always starts executing at the first line of the file. Be aware that there are no intrinsic differences between batch files that initiate jumps or calls, batch file subroutines and the targets of batch file jumps. A single batch file can do and be all of them. One batch file can be called as a subroutine, serve as the target file for a jump, issue subroutine calls and issue jumps commands. The distinction between a jump and a call has to do with the specific command used. It has nothing to do with the type of file it is in. They are all just batch files. A batch file jump simply requires listing the target batch file on a line by itself, as follows. batch_file_name The syntax for a conditional batch file jump goes something like this. IF [NOT] condition batch_file_name DOS does not load a secondary command processor as part of a jump operation. Therefore, the target batch file has access to the same DOS environment as its parent batch file. This can be either the primary environment or a copy of the environment depending on which command processor is highest in memory. A series of batch files may jump to and from each other. As soon as any one of the batch files executes its last command, the process stops. All of the batch files involved in these jumps will have access to the same copy of the DOS environment and each will respond to changes made in the environment made by any one of them. LISTING 1 shows three ways to use jumps. These are only three out of an infinite number of possibilities. The art of batch file programming lies in finding innovative ways to use these tools. This will only get you started. ============================================================================ LISTING 1 - Three Different Types of Batch File Jumps A) The simple batch file jump to a batch file named TEST1.BAT. \ Start of the parent batch file. / TEST1 \ If there are any more lines in this batch file, they will never execute because of the unconditional jump to TEST1.BAT. / B) A conditional batch file jump to a batch file named TEST2.BAT. \ Start of the parent batch file. / IF "%T1%"=="JUMP" TEST2 \ The rest of this batch file will not execute if the batch file sets the environment variable T1 equal to JUMP before testing it. / C) A compound conditional batch file jump to a batch file named TEST3.BAT. In this particular case the batch file jumps to TEST3.BAT with a parameter whose value depends on the value of a replaceable parameter. \ Start of the parent batch file. / IF "%1"=="VER" TEST3 verify IF "%1"=="CONS" TEST3 consolidate IF "%1"=="PRINT" TEST3 print \ The rest of this batch file will not execute if the replaceable parameter, %1, satisfies any of the three test conditions. / ============================================================================ BATCH FILE SUBROUTINE CALLS USING THE CALL COMMAND Starting with DOS 3.3, batch file subroutines can be executed using the CALL command. The simple subroutine CALL uses the following syntax. CALL batch_file_name The CALLed subroutine retains the same ECHO status as the calling batch file. The CALLed subroutine uses the same command processor as the calling batch file and has access to the same DOS environment. That means that the batch file and the subroutine can pass data back and forth to each other using DOS environment variables. The calling batch file can use an IF [NOT] == test to find out which environment variables were created or changed by the subroutine. This type of subroutine call can use an IF commands to make a conditional subroutine calls. The syntax is as follows. IF [NOT] condition CALL batch_file_name This type of subroutine call can use a FOR IN DO command to execute a series of subroutine calls to different subroutines. The syntax looks something like the following, where the batch file subroutines TESTING.BAT, PRINTING.BAT and ARCHIVE.BAT execute, consecutively. FOR %%X IN (testing printing archive) DO CALL %%X The following command could execute TEST1.BAT, TEST2.BAT and TEST3.BAT, consecutively. FOR %%L IN (1 2 3) DO CALL test%%L This type of subroutine call can also use a FOR IN DO command to execute a consecutive series of subroutine calls to the same subroutine. The syntax looks something like the following example, which executes the subroutine seven times. FOR %%R IN (x x x x x x x) DO CALL batch_file_name Note that the %%R does not appear in the batch file name. The same batch file gets executed each time. The number of subroutine calls equals the number of items in the (set). Because of the 127 character limit for the command line length, this technique limits the number of repetitions to about 48 to 50. Alternatively, the members of the (set) could be a series of different parameters for the subroutine, like STATS.BAT in the following case. Then the FOR IN DO command would look something like this. FOR %%R IN (hits runs errors) DO CALL stats %%R BATCH FILE SUBROUTINE CALLS USING COMMAND.COM Prior to DOS 3.3, this was the only way to execute batch file subroutines. It still works in DOS 5.0. The simple subroutine call using COMMAND.COM uses the /c switch in the following syntax. COMMAND /e:yyyy /c string (yyyy = 160 to 32,768) The /c switch forces COMMAND.COM to interpret the string that follows the switch as a command to execute. That command can be a batch file name, an application program, or a DOS command. Using the /c switch also forces COMMAND.COM to unload itself after is finishes processing the command. The version of the COMMAND command to use depends on the DOS version. DOS 3.0, DOS 3.1 and DOS 3.2 will not work with the /e switch. With DOS 3.3 and above the /e switch is optional. The subroutine does not retain the ECHO status of the calling batch file. An ECHO off followed by a CLS command (or @ECHO off in DOS versions 3.3 and above) should be included at the beginning of any subroutine called using COMMAND.COM. The /c switch specifies that a secondary command processor load and execute the command. It will unload itself when it finishes processing the command. Since the command is operating under the control of a secondary command processor, it only has access to a copy of the DOS environment. If the command is a batch file subroutine, it can read the environment but it cannot pass data back to the calling batch file using environment variables. This is the major difference between subroutines called with the CALL command and those called with COMMAND.COM. One way for these subroutines to pass information back to the calling batch file is by creating temporary files. The calling batch files can check for the presence of these temporary files using IF [NOT] EXIST tests. It is easy to create a zero byte file, with any required name and extension, by redirecting the output of a blank REM statement to that file name. For example, the subroutine might include a series of IF statements that conditionally create one or more files this way. Such IF statements use the following syntax. IF [NOT] condition REM >[drive:][path]filename.ext After the return, the calling batch file can use IF [NOT] EXIST tests to find out which files were created. It is a good idea to create temporary files in a single directory with a common and distinctive extension. That makes it possible to use the following command to get rid of all the zero byte temporary files in one step. DEL [drive:][path]*.ext An extension like ZRO might be a good one to consider. The copy of the DOS environment the subroutine gets may not be as big as the original DOS environment specified with the SHELL command in CONFIG.SYS. It is only large enough to hold the current DOS environment variables, rounded up to the nearest multiple of 16 bytes, or 256 bytes, whichever is larger. If the subroutine makes extensive use of the DOS environment, the /e switch can create a larger environment size for the secondary command processor. Alternatively, delete environment variables from the copy of the environment. Clearing variables from a copy of the environment will not affect the primary DOS environment. Do not erase the COMSPEC variable, however. This type of subroutine call can use an IF command to make a conditional subroutine call. The syntax is as follows. IF [NOT] condition COMMAND /c batch_file_name This type of subroutine call can use a FOR IN DO command to execute a series of subroutine calls to different subroutines. The syntax looks something like the following, where the batch files HOP.BAT, SKIP.BAT and LEAP.BAT execute, consecutively. FOR %%X IN (hop skip leap) DO COMMAND /c %%X Alternatively, a FOR IN DO command like the following can be used to execute the batch files TESTX.BAT, TESTY.BAT and TESTZ.BAT, consecutively. FOR %%L IN (x y z) DO COMMAND /c test%%L This type of subroutine call can also use a FOR IN DO command to execute a consecutive series of subroutine calls to the same subroutine. The syntax looks something like the following example, which executes the subroutine five times. FOR %%G IN (z z z z z) DO COMMAND /c batch_file_name Note that the %%G does not appear in the batch file name. The same batch file gets executed each time. The number of subroutine calls equals the number of items in the (set). Because of the 127 character limit for the command line length, this technique limits the number of repetitions to about 48 to 50. Alternatively, the members of the (set) could be a series of different parameters for the subroutine, like MEMO.BAT in following case. Then the FOR IN DO command would look something like this. FOR %%G IN (Bob Carol Ted Alice) DO COMMAND /c memo %%G HOW TO EXIT FROM A SUBROUTINE The options available for a normal exit from a batch file subroutine can depend on the method used to call the subroutine. One sure way to exit from all subroutines is to branch to the last line of the subroutine. When the subroutine ends, the calling batch file resumes execution at the line following the call to the subroutine. This branching operation can slow down the entire process in a long subroutine; the command processor has to scan the entire subroutine to find the last line before it can terminate and return. A jump to a short batch file is a more elegant way to terminate a subroutine. Jumping does not, by itself, trigger the return . It is finding the last line of a batch file that triggers the return to the calling batch file. The trick is that it does not have to be the last line of the called subroutine that executes. It can be the last line of any batch file. There are times when it is quicker to jump to a short batch file and let it end rather than wait for the command processor to find the last line of a long subroutine. For DOS 3.3 and later, Paul Somerson, in his book "PC Magazine DOS Power Tools, 2nd Edition," recommends jumping to the short batch file, which he calls DO.BAT, containing the following single line. @ECHO %1 %2 %3 %4 %5 %6 %7 %8 %9 To terminate a subroutine anywhere other than at the end, include the following conditional test in the appropriate places. IF [NOT] condition DO message The message can contain up to nine words. DO.BAT displays the message just before the calling batch file restarts. For DOS versions before 3.3, the initial ampersand should be omitted from DO.BAT. The message is optional. This technique works for subroutines called with CALL or COMMAND.COM. A redirected REM command will create a zero byte batch file. Jumping to that file triggers a return to that calling batch file, too. The EXIT command will only terminate subroutines called with COMMAND.COM. Subroutines called with the CALL command do not respond properly to the EXIT command. If the active command processor is the primary command processor, they just ignore the EXIT command. If the batch file issues a CALL command and a secondary command processor already resides in memory, the EXIT command forces the secondary command processor to unload itself. A return to the batch file that issued the CALL command is impossible after that. The EXIT command forces a return to the batch file that loaded the secondary command processor, instead. With a manually loaded batch file there is no calling batch file to return to; batch file execution ends. In either event, execution cannot resume in the batch file that issued the CALL command because the subroutine stack disappeared when the secondary command processor unloaded itself from memory. There might be rare cases where this is the desired result. Usually, it is just a mistake. The best techniques for returning from a subroutine are jumping to a short batch file or branching to the end of the subroutine. Both techniques work no matter how the calling batch file executed the subroutine call. An EXIT command always forces the active secondary command processor to unload from memory. That unconditionally terminates any batch file it is running and restarts the batch file that loaded the secondary command processor. If the primary command processor is running the batch file, EXIT has no effect, because the primary command processor will not unload from memory. Using EXIT is only slightly faster than using DO.BAT but it does not include the option to display a message. Also, it forces the programmer to keep track of two types of subroutines; those called with CALL and those called with COMMAND.COM. Use of the EXIT command should be discouraged unless the subroutines are only going to run on PCs using DOS versions prior to 3.3. That is no longer a safe assumption and EXIT is falling out of general use by batch file programmers. NESTING FOR IN DO COMMANDS DOS does not allow nesting FOR IN DO commands. By using a secondary command processor, however, you can work around that limitation. The example lines shown in LISTING 2 can print out all of the batch files in the current directory . (As an aside, note that this batch sequence simplifies the DOS PROMPT to make the printouts easier to read and then restores the PROMPT after it finishes printing.) ============================================================================ LISTING 2 - Example of "Nested" FOR IN DO Commands set oldp=%prompt% prompt=$g FOR %%A IN (*.bat) DO C:\COMMAND /c FOR %%B IN (ECHO TYPE) DO %%B %%A >prn prompt=%oldp% set oldp= ============================================================================ This sequence first ECHOs the name of a member of the set (*.BAT) to the printer and then TYPEs its contents on the printer. ECHOing the name simply creates a header for the listing that follows. If you have a little utility called FF.COM, you can include it in the second set, (ECHO TYPE FF), and get each batch file to print out on a separate page. This sequence repeats until it has printed all the members of the set (*.bat). Nesting FOR IN DO commands is a little tricky but it opens up some interesting new possibilities for creative batch file programmers. Sometimes its helpful to experiment at the DOS prompt. When working at the DOS prompt, remember to use only one percent sign in front of the variable name. Assume you have a group of batch files that handle the quarterly accounting summaries. Here are the names. EXP1.BAT INC1.BAT NET1.BAT EXP2.BAT INC2.BAT NET2.BAT EXP3.BAT INC3.BAT NET3.BAT EXP4.BAT INC4.BAT NET4.BAT What is the order of execution when the following nested FOR IN DO command executes these twelve batch files? FOR %%T IN (EXP INC NET) DO COMMAND /c FOR %%Q IN (1 2 3 4) DO CALL %%T%%Q What is the order of execution when the following nested FOR IN DO command executes these same twelve batch files? FOR %%A IN (1 2 3 4) DO COMMAND /c FOR %%B IN (NET EXP INC) DO CALL %%B%%A REBOOT, REBOOT AND REBOOT AGAIN Anyone who wants use their own batch files to juggle multiple configurations quickly discovers that it would be really nice if DOS provided a utility to automatically reboot the PC after the batch file swaps the new and old versions of CONFIG.SYS and AUTOEXEC.BAT. Here is an assortment of reboot utilities to consider. The first method I learned uses a one line BASICA script. REBOOT.BAS is an ASCII text file containing the following single line. 10 DEF SEG=&HF000:RB=&HE05B:CALL RB More recently, Paul Somerson reported using the equivalent decimal version of the same BASICA commands, as follows. 10 DEF SEG=61440:R=57435:CALL R To reboot the PC, simply execute the following line from the command line or in a running batch file. BASICA reboot Those running DOS 5.0 will have to add BASICA to the version table and install SETVER.EXE before it will work. Both BASICA scripts work and both perform a cold boot of the system. Some users will have a problem making them work, however. The problem I found occurs only when QEMM386 or 386MAX is installed. DOS's HIMEM.SYS does not cause the same problem. There are many other choices, so if you use one of those memory managers, don't get discouraged yet. Any number of commercial applications also include a reboot program. For example, MACE Utilities, Software CAROUSEL, and BOOTCON all have their own reboot utility. These particular programs all perform a warm boot of the system. Quite a few PC owners, though they may not know it, already have a reboot utility stored somewhere on their hard disk. The PC Utilities, a large and growing collection of utilities available on PC MagNet, on diskettes from the NTPCUG, or by purchasing a copy of Paul Somerson's book, "PC Magazine DOS Power Tools, 2nd Edition," includes REBOOT.COM. This utility stops to offer the choice of Warm-Boot, Cold-Boot or ESC to abort the reboot operation. PC Magazine printed a script file for another utility with the same name, REBOOT.COM (which performs a warm boot, only), in the December 17, 1991 edition on page 429. PC/Computing printed script files for WARM.COM and COLD.COM in the December 1991 edition on page 343. The scripts REBOOT.SCR, WARM.SCR and COLD.SCR appear in LISTING 3. I've also included in LISTING 3 a script for the cold booting version of REBOOT.COM, which I've named REBOOTC.SCR. ============================================================================ LISTING 3 - Scripts for four utilities that can reboot a PC Script File REBOOT.SCR Script File REBOOTC.SCR N REBOOT.COM N REBOOTC.COM A 0100 A 0100 MOV AX,40 MOV DS,40 MOV DS,AX MOV DS,AX MOV WORD PTR [72],1234 MOV WORD PTR [72],0000 JMP FFFF:0000 JMP FFFF:0000 RCX RCX 10 10 W W Q Q Script File WARM.SCR Script File COLD.SCR N WARM.COM N COLD.COM E 0100 B8 40 00 8E C0 26 C7 06 E 0100 B8 40 00 8E C0 26 C7 06 E 0108 72 00 34 12 EA 00 00 FF E 0108 72 00 00 00 EA 00 00 FF E 0110 FF E 0110 FF RCX RCX 0011 0011 W W Q Q All these script files begin with the N command. They all include a blank line ahead of the RCX command and they all need a blank line after the Q command. ============================================================================ To create the executable programs from these scripts, first create the scripts in the TESTUTIL directory. Then, one at a time, execute the following command, substituting different script file names each time. DEBUG temp.bat CALL temp DEL temp.bat ECHO Good Morning %ASK%. That scheme stores the user's name in the environment. QUERY always uses the same variable name. The next time a batch file uses QUERY it will overwrite the user's name. To save the user's name, execute two additional lines, as follows. SET name=%ASK% SET ASK= The script file used to create QUERY.COM appears in LISTING 4. ============================================================================ LISTING 4 - Script File QUERY.SCR N QUERY.COM E 0100 FC B4 0E B3 0F BE 81 00 AC 3C 20 74 FB 3C 0D 74 E 0110 05 CD 10 AC EB F7 33 C9 BF 80 00 32 E4 CD 16 0A E 0120 C0 74 3D B4 0E 3C 08 74 42 3C 0D 74 13 3C 03 74 E 0130 2A 3C 20 72 E6 83 F9 7F 74 2A AA 41 CD 10 EB DB E 0140 CD 10 B0 0A CD 10 B4 09 BA 7B 01 CD 21 B4 02 E3 E 0150 0A BE 80 00 AC 86 D0 CD 21 E2 F9 B8 00 4C CD 21 E 0160 0A E4 74 F7 B8 07 0E CD 10 EB B0 E3 F7 B0 08 CD E 0170 10 B0 20 CD 10 49 4F B0 08 EB C1 53 45 54 20 41 E 0180 53 4B 3D 24 RCX 84 W Q Don't forget the blank line after the Q. Create QUERY.COM by issuing the following command at the DOS prompt. DEBUG filename.ext) [CTRL]-[ALT]-[DEL]