Examples

Many of these examples were created in response to questions posed in the MSDOS forum on Compuserve, or in alt.msdos.batch or comp.os.msdos.programmer. Others are from or after programs created in the course of my daily work. All have been tested, at least in some environment. Some are rewrites of interesting programs by someone else, that I saw somewhere (but don't have a copy of). In most of these examples, a complete batch file, even if just a trivial example, is indicated by @echo off at the beginning and :end at the end, even if one or both are unnecessary from the structural point of view, while fragments generally have neither.

Keep in mind that here, as always, my use of COMMAND to obtain a secondary command processor is specific to unaltered DOS. In nearly all cases, the generic equivalent is %comspec%, which launches whatever command processor is actually in use - in fact, I usually use that syntax myself when there is any chance that it might not be COMMAND.COM. Another reason for preferring the %comspec% syntax in real life is that that variable often points to a copy of the command processor in a RAMDRIVE, which speed things up quite a bit. I have used the explicit syntax, COMMAND, here, because none of this code has been tested with anything else, at least not by me. In fact, the object of this entire exercise is to explore and illustrate the behavior of COMMAND.COM, as it exists in MSDOS 6.22. Catering to other command processors is necessary in the real world, but this is not the real world - it is more like an experimental laboratory.


DOBAT.BAT is simply

 @%1 %2 %3 %4 %5 %6 %7 %8 %9
It lets you execute an arbitrary batch command from another batch file or from the command line. It's main use is to assist another batch file in deleting itself, which it can do by invoking DOBAT with the argument string "del %0.bat", but it can also be used as a general purpose single line batch file in other circumstances that don't occur to me at the moment. You cannot use it to extract or set an environment variable, or to redirect the output of a command. It can be used from the command line to defeat the prompts of some of the DOS 6 commands, where the prompt doesn't occur if the command is invoked from a batch file (for example, COPY, when the target already exists). I keep a copy of this one in the path.

Variations on IF EXIST filespec command:

Note that foo is a metasyntatic variable and usually stands for a filename, or partially or fully qualified filespec, with or without wild cards. Note also that those commands that generate screen output on success are redirected to the NUL device to suppress that display.

 if exist foo.bar del foo.bar
can be expanded to

 if exist d:\foo\*.* echo y | del d:\foo\*.*
(Note: Thanks to Daren M. Fowler for spotting a typo in the above line: the '|' 
character had been mistyped as '\'.)
Here, ECHO Y is used to generate the "ok" response to DEL *.*, which is then piped ('|') to the command. It is essential that when *.* is used in the argument to DEL, the full path to the directory of interest be used - you cannot be absolutely certain that the default directory is what you think it is: the directory change may have failed, the directory may not exist, Windows may have changed the default directory without your knowledge, etc.

 if exist foo.bar copy foo.bar foo.baz > nul
This can also be used to make a copy in another directory

 if exist foo.bar copy foo.bar d:\baz\*.* > nul
or just to rename the file in situ

 if exist foo.bar ren foo.bar baz.qux
If foo contained a path, the path can be omitted from baz.

Converting a string (the first command line argument in this case) to all caps by passing it through the PATH command:


 @echo off
 set oldpath=%path%
 path %1
 set string=%path%
 set path=%oldpath%
 set oldpath=
 echo %1 was converted to %string%
 set string=
 :end

This one was created in response to a question about how to get the date into a batch file (without the '-'s) and perform arithmetic operations on it based on the number of non-work days past since the last time the program was run, in order to create a filename based on the date of the previous work day. I saw that the request had no possible solution in a finite amount of code, so I took a different tack: I noticed that the desired filename was built from the date that was the same as the date stamp of the previous day's file. The desired filename was in the form VGddmmyy.BAT, where dd, mm, and yy stand for the two digit day, month, and year numbers, but I already had code to construct the yymmdd string, which sorts correctly (until 000101), so I used that format in the following example. The pattern file from which the new one is to be made is DOWNLOAD.BAT.


 @echo off
 %1 %2
 ren vg*.bat *.
 for %%a in (vg*) do %0 goto pass1 %%a
 :pass1
 dir %3 /-w /-s /-b /-l | find "VG" > }2{.bat
 del vg*.
 echo set fname=%%2> %3.bat
 echo if not "%%4" == "" set fname=%%3>> %3.bat
 call }2{.bat
 for %%a in (%3 }2{) do del %%a.bat
 for %%a in (mm dd yy ss) do set %%a=
 set flag=ch
 for %%a in (m m h d d h y y) do call %0 goto pass2 %%a
 set fname=%yy%%mm%%dd%%
 for %%a in (mm dd yy ss flag hh) do set %%a=
 type download.bat > vg%fname%.bat
 set fname=
 goto end
 :pass2
 for %%b in (/%fname%!) do call %0 goto pass3 %%b %3
 goto end
 :pass3
 goto %flag%
 :ch
 set ss=%ss%%3
 set %4%4=%ss%
 set flag=str
 if "%3" == "-" set ss=
 goto end
 :str
 set fname=%3
 set flag=ch
 goto end
 :end

A more general form is

YYMMDDG.BAT


 @echo off
 if "%1" == "}{" goto %2
 rem > }1{.bat
 if not "%1!" == "!" copy %1 }1{.bat > nul
 dir }1{ /-w /-s /-b /-l | find "}1{" > }2{.bat
 echo set ddate=%%3> }1{.bat
 call }2{.bat
 del }1{.bat
 del }2{.bat
 set mm=
 set dd=
 set yy=
 set ss=
 set flag=ch
 for %%a in (m m h d d h y y) do call %0 }{ pass2 %%a
 set ddate=%yy%%mm%%dd%%
 set mm=
 set dd=
 set yy=
 set ss=
 set flag=
 set hh=
 set fname=
 goto end
 :pass2
 for %%b in (/%ddate%!) do call %0 }{ pass3 %%b %3
 goto end
 :pass3
 goto %flag%
 :ch
 set ss=%ss%%3
 set %4%4=%ss%
 set flag=str
 if "%3" == "-" set ss=
 goto end
 :str
 set ddate=%3
 set flag=ch
 goto end
 :end

which sets DDATE to the date in yymmdd format. Note that if it is passed a filename as an argument (no sanity checking is performed) it uses the date stamp of that file, otherwise it uses the current date

Now, where can we take this - what are the real limits for naming files with time information? We obviously can create directories for the date (md %ddate%) but then what? The technique can be modified to work with the time stamp as well, and it just so happens that the total non-punctuation characters in the combined date and time (when derived from a file's date and time stamps) exactly equals the number of characters allowed in a filename plus extension. The DIR entry for a file on one of my machines is "TEST BAT 178 04-24-96 8:43p" which could be turned into a file name of 96042408.43p (it's actually harder without the forced leading zero in the single digit hour). We already have the date from YYMMDD.BAT above, so we need something to deal with the time and something to put it all together. The first following example is the time extractor, which uses a bang (!) counter to handle the leading zero problem. The next is a simple program to take a filename, generate the date/time name from the pieces provided by the two called utilities, and rename the file to its date/time stamp. The time extractor will be named HHMM.BAT.



 @echo off
 if "%1" == "}{" goto %2
 rem > }1{.bat
 if not "%1" == "" copy %1 }1{.bat > nul
 dir }1{ /-w /-s /-b /-l | find "}1{" > }2{.bat
 echo set ttime=%%4> }1{.bat
 call }2{.bat
 del }1{.bat
 del }2{.bat
 set rawtime=%ttime%
 :start
 set hh=
 set mm=
 set ss=
 set count=
 set flag=ch
 set try=ok
 for %%a in (h h c m m m) do call %0 }{ pass2 %%a
 if not %try% == fail goto len_ok
 set ttime=0%rawtime%
 goto start
 :len_ok
 set ttime=%hh%%mm%
 set ss=
 set flag=
 set fname=
 set cc=
 set count=
 set rawtime=
 set try=
 goto end
 :pass2
 for %%b in (/%ttime%!) do call %0 }{ pass3 %%b %3
 goto end
 :pass3
 goto %flag%
 :ch
 set count=%count%!
 set ss=%ss%%3
 set %4%4=%ss%
 set flag=str
 if "%3" == ":" set ss=
 if "%3" == ":" if %count% == !! set try=fail
 goto end
 :str
 set ttime=%3
 set flag=ch
 goto end
 :end

Questions:
Just what is that bang counter doing anyway?
Why is the way the possible need for a leading zero is handled here very much more efficient than the more obvious method of counting the characters in the time string?
What is the method of dealing with the possible need for a leading zero?
Why is it harder to do this without forcing a leading zero if the hour is a single digit?

Note that this code does not clean up the hour and minute variables (hh and mm) - they will be needed individually (but the composite will be ignored). The cleanup occurs in the outer batch file below.



 @echo off
 call yymmdd %1
 call hhmm %1
 ren %1 %ddate%%hh%.%mm%
 set ddate=
 set ttime=
 set hh=
 set mm=
 set mm=

A few questions:

In all of these examples, there are variables that are not declared or used but are generated and cleaned up - HH in the date programs and CC in the time one, and SS in both. What functions do these serve?


This code compares the date stamps of two files passed as arguments and executes additional code based on the comparison. It has the restriction that both files must have filename extensions. Changing %%3 to %%2 changes that to the restriction that they must both *not* have extensions. Adapting the code to handle any combination is left as an exercise for the reader (hint: it is done in }{.BAT)


 @echo off
 copy %1 }{.}}}                  
 dir }{.}}} | find "}{" > }1{.bat
 echo set date1=%%3> }{.bat      
 call }1{                        
 copy %2 }{.}}}                  
 dir }{.}}} | find "}{" > }1{.bat
 echo set date2=%%3> }{.bat      
 call }1{                        
 del }{.bat                      
 del }1{.bat                     
 del }{.}}}                      
 if %date1%==%date2% goto equal  
 :not equal                      
 call nequal.bat                 
 goto end                        
 :equal                          
 call equal.bat
 

There is no error checking of the argument - the calling program or user is responsible for seeing to it that the arguments are valid filespecs.


One FAQ is "how do I make a batch file execute a program at a specific time". This does that but must be run from a RAMDISK - to say that it is disk intensive is a serious understatement. The idea is that we can get the more or less current time without the seconds in h:mmp form where p is either 'a' or 'p' for AM or PM, h is the hour (1 - 12), and mm is the minute from the directory entry for a file that we just created a fraction of a second before. This can be compared as a string with a pattern passed as an argument or hard coded into the batch file. This batch file writes and reads files continuously until the strings match, then it breaks out of the loop and calls foo, which stands for the name of the program you want to run. The program then terminates. You can make it loop back and wait for the same time tomorrow by replacing :end with goto loop.
The user or calling program invokes this with a time in the same format as the target time as the only argument. There is no error checking, so to avoid a syntax error or endless looping it is essential that you make sure the argument is a valid time stamp for a file and is one that will eventually be generated.


 @echo off
 set pattern=%1
 echo set now=%%4> }{.bat
 :loop
 rem > }{.}}}
 dir }{.}}} | find "}}}" > }1{.bat
 call }1{
 if not %pattern%==%now% goto loop
 call foo
 :end
It would be very unwise to run this off a hard drive and nearly senseless to run it off a floppy (which could not be expected to survive the beating for very long). This is an academic exercise, not reasonable code.
The directory listing is filtered through FIND to isolate the line containing the extension of the just created file (created by the redirected REM), which interestingly enough has }{ as its first field and }{ is exactly what is needed to invoke }{.BAT with the rest of the directory listing string as its arguments - }{.BAT contains the command

 set now=%4
and it's fourth argument just happens to be the file creation time of }{.}}}, which is the nearly current time.

A similar program (using %%3 instead of %%4) will run a program on a specific date. The two can be combined to run the program at a specific time on a specific date.


It is possible to save and restore the current drive and directory, but it's so intricate that it is seldom done. There are utilities that push and pop directories and are much more reliable and easier to use.

 dir | find "Volume in" > }1{.bat
 echo set drive=%%3> volume.bat
 dir | find "Directory" > }2{.bat
 echo set dir=%%2 > director.bat
 call }1{
 call }2{
 for %%a in (}1{ }2{ volume director) do del %%a.bat
to save the current drive and directory, and
 %drive%:
 cd %dir%
 set drive=
 set dir=
to restore. Note that there must not be a space between '3' and '>' in "echo set drive=%%3> volume.bat" to avoid having a space between the drive letter and the colon in "%drive%:". Note that if a second instance of this runs it will overwrite the data from the first, unless the second is run in a secondary shell or each batch file uses different names for the drive and directory environment variables.
Normally, one uses a non-returning recursion approach to extract the drive letter from a path or filespec, but this example of extracting the drive letter from the TEMP= variable is non-recursive. The idea is that you can easily change to the TEMP directory (CD %temp%) but that an attempt to use the variable as a command in hopes that it will change drives just gets you an error message. Here we extract the tail and successively compare it to TEMP= with each possible drive letter in front - the one that works gets saved. Of course we have to normalize %temp% to upper case and use upper case trial letters because string comparisons are case sensitive.

 @echo off
 if "%temp%"=="" goto error
 set oldpath=%path%
 path %temp%
 for %%a in (/%path%) do set tail=%%a
 set temp=%path%
 set path=%oldpath%
 set drive=
 for %%b in (A B C D E F G H I J K L M N O P Q R S T U V W X Y Z ) do if %temp%==%%b%tail% set drive=%%b:
 if "%drive%"=="" goto error
 %drive%
 goto end
 :error
 echo Program failed - problem with TEMP=%temp%
 :end
 set tail=
 set drive=
Of course you can leave out any letters that are impossible in any given case, and normally one could leave out A: and B:.

The higher ups who control some parts of our LAN have a number of batch files that set TEMP=S:\, and then map S: to a read only drive - we have to test to see if the TEMP directory is in fact writable (we don't really care where it is as long as it can be used for transient files. This code tests for that - and for the special case where the directory is writable but not erasable (another of the bright ideas from the higher ups). Using the same test file name in every instance of the test program limits the buildup of orphaned files.

The example program sets the test variable to a string, but the real ones just set it to a useable/not-usable boolean flag. Note that the variable is not cleared at the end of this code - if used as a called routine, it should not clean it up, but if the code is embedded in another program it should ( set w= ).


 @echo off
 set w=not writable
 ctty nul
 rem > %temp%\}{.{{{
 if not exist %temp%\}{.{{{ goto done
 set w=writable
 del %temp%\}{.{{{
 if exist %temp%\}{.{{{ set w=writable but not erasable
 :done
 ctty con
 echo TEMP=%temp% and is %w%
 :end

There are some problems with this code - particularly the point that it crashes with a blank screen if TEMP points to a drive with removable media that has been removed or if there are any other errors that cause an ARF (Abort, Retry, Fail) error message. This problem can be dealt with, but not easily: the test can be made in a secondary command processor launched with the /f switch to automatically fail ARF situations, but then the problem of getting the information back across the gulf between the primary and secondary command processors without a common environment or a known writable directory (if no other way, by using the date to convey a small amount of information.


Someone asked about getting DIR listings for a list of extensions - I have expanded that to include named categories of files in this example. While it might seem appropriate to put this in the writeup on DIR, I put it here and referenced it from several of the other places it might belong - it illustrates far too many esoteric features of batch language to warrant putting it with any specific command.

The syntax if of the type


 dirx drive:directory 
or
 dirx drive:directory 
There is one gotcha in the syntax: the directory must not end with a '\' - use c: for the root of c: (C:\) and c:. for the current directory on C: - the latter expands to C:.\*.foo (foo is the extension) which is the current directory on the C: drive.

Type names are always at least four characters so that they can't conflict with real extensions of one, two, or (usually) three characters. The default is type EXEC (executable programs), namely EXE, COM, BAT, and PIF. If any arguments are used, the first must be the path to the directory, often just '.' to indicate the default directory. The only other type names defined here (in the interest of brevity) are IMAGE: GIF, JPG, TIF, BMP, and WPG - just to show how it's done so you can add your own lists; and NULL: files with no extension. The latter adds a bit of complication to the program. NULL is not really a type - it's a place holder for a blank - so it can be combined with other extensions in a list; regular type names cannot be combined into lists.

DIRX.BAT
 @echo off
 if !%1==!}{ goto %2
 set oldpath=
 set exts=
 if !%2==! goto default
 set oldpath=%path%
 path %2
 if %path% == EXEC set exts=COM;EXE;BAT;PIF
 if %path% == IMAGE set exts=GIF;JPG;TIF;BMP;WPG
 :: tests for additional types go here
 if not !%exts%==! goto continue
 :default
 set exts=%2;%3;%4;%5;%6;%7;%8;%9
 :continue
 if not !%oldpath%==! set path=%oldpath%
 if !%2==! set exts=COM;EXE;BAT;PIF
 command /e:1024 /c %0 }{ pass2 %1 | more
 set oldpath=
 set exts=
 goto end
 :pass2
 set base=
 set wip=
 set oldpath=%path%
 path %exts%
 set exts=%path%
 set path=%oldpath%
 set base=%3
 for %%a in (%exts%) do call %0 }{ pass3 %%a
 goto end
 :pass3
 set wip=%3
 if %3 == NULL set wip=
 if exist %base%\*.%wip% call %0 }{ pass4 %3
 goto end
 :pass4
 dir %base%\*.%wip% /a:-d | find ":" | find /v "\"
 :end

All arguments are optional - if omitted, it looks for *.EXE, *.COM, *.BAT, and *.PIF in the default directory; if used, the first argument is the drive:\directory and any others are the extensions (or name of a type) to search for.

This program pages the display - if that is not needed it can be simplified somewhat. If you want to redirect the output to a file, replace the "| more" with "> foo", where foo is the target file (not in the target directory or not having a searched for extension).

Almost every line of this batch file would make a good exam question - the task itself would be a good challenge for extra credit - the above solution would almost do as a final exam all by itself. There are few students of batch language who can be expected to understand that code, so I have provided a line-by-line explanation as a separate file.

There are still a few enhancements that might be added: standard DIR header and footer (minus the total byte count for the files), preset lists passed in an environment variable, lists of type names, user specified sorting of the individual sublists and/or of the combined list, etc. These are left as exercises for the reader. They are not impossible - even totaling the byte counts is possible - but they do add significant amounts of both code and complication, and this example is complicated enough already (four layers and two command processors deep).


This knock off tells whether or not a directory exists. It should work with most networks, but has a number of drawbacks if there is any possibility that the root directory is empty or the drive doesn't exist, or any of the other cases that generate ARF (Abort, Retry, Fail) errors apply - it will stop cold waiting for user input.


 @echo off
 dir %1\ /a:d | find /i "%2" > nul
 if errorlevel 1 goto notfound
 echo Directory %2 exists
 goto end
 :notfound
 echo Directory %2 does not exist
 :end

The first argument is the drive letter (without the colon) and the second is enough of the path to the directory to ensure unique identification, usually the entire path. This code is fairly fragile so should not be used except from another batch file, where the drive is known to contain at least one directory, and where the drive has non-removable media (hard disks and network drives (if the batch file is on the network so it won't be run unless the network is connected)). A much more sophisticated and robust directory checker is in the "External Commands" section under "UNDELETE".




From time to time someone asks if it is possible to create random filenames in batch programs. While it might seem that something as deterministic as a batch language would not even have a randomizer, and it doesn't ... but the operating system does, and can be induced to create transient files with random names with pipe syntax - the files go into the TEMP directory and vanish at the end of the pipe. There is one well known series of errors and inexperience that generates another question: "what are the two extra files I see when I pipe DIR through MORE?". The answer to that one provides the mechanism for capturing one of those filenames - the TEMP environment variable wasn't set, so DOS and MORE used the default directory as a place to put the transient files used by the pipe, and since the pipe is set up before the DIR command is executed, the transient files show up in the listing (as empty files). Since we have to have at least two fields for the next step to work, we can't just use the /b switch to DIR to isolate the filenames - we have to pipe through FIND, and so don't actually need MORE.

 @echo off
 if not exist }{\nul md }{
 cd }{
 set oldtemp=%temp%
 set temp=.
 dir | find " 0 "> ..\}{.dat
 set temp=%oldtemp%
 cd ..
 echo. >> }{.dat
 date < }{.dat | find "En" > }{.bat
 echo set fname=%%4 > enter.bat
 call }{
 for %%a in ( }{.bat }{.dat enter.bat ) do del %%a
 rd }{
 echo Random filename = %fname%
 set fname=
 set oldtemp=

What we did was to isolate ourselves in an empty directory so the only files to be seen are the transients, then filter the listing for zero length files (a '0' with spaces on both sides). The list containing only the filenames then has a blank line appended and is fed to DATE, which barfs it back - but with "Enter new date (mm-dd-yy):" inserted at the front of the line. We capture that error stuff and then create a batch file that can be invoked by the first word in the first line. That batch file contains only an instruction to SET FNAME to the field in the remainder of the DATE error that contains the filename: %4 in DOS/Win95 or %5 in NT4. CALLing the first batch file causes it to invoke and jump to the second, which sets the variable and returns to the master batch program. See "List Processing" for more about using DATE to extract lines form files.


To be continued.

  ** Copyright 1995,1996, 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