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.
@%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:
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
echo if not "%%4" == "" set fname=%%3>> %3.bat ?
goto %flag% ?
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.
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.
@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