There are enough differences among the various flavors of "DOS" in real DOS, Win95, and NT4 that programs written for one often fail, sometimes for obscure reasons, in one or both of the others. The first illustration is something I used to prove a point, but also to explore some of the differences. A number of people have expressed interest in the program even though it's still (9 Feb. 1998) in beta testing (I need to *know* what breaks it) that I have decided to publish the code, comments, and explanatory material much too soon, well before I understand some of it myself. This is very preliminary stuff and will be rewritten later, when the dust settles (copyright considerations also played a role in that decision).
The program was inspired by a thread in alt.msdos.batch that began with a question about extracting the individual directory names from a fully qualified directory specification. One thing led to another, and I decided that I needed to explore the possibilities.
Several problems immediately presented themselves, one of which was that the user hadn't indicated what operating environment he was using - "Well, it shouldn't be too hard to make it work in all three." - famous last words. It was a nightmare: short vs. long names, spaces in names, different behaviors of DIR, case insensitivity, the pattern is a variable but the DIR listing is in a file - how to look for the contents of a file in a variable, list processing using the LOADFIX/DATE scheme is broken after MSDOS6.22 due to lack of LOADFIX, string processing is broken after MSDOS6.22 because of lack of the '/' switch to FOR but fixed to some extent in NT4 with extensions to FOR, and so forth, and so on. This little devil of a program is the result:
ENTER.BAT @echo off
if not %pass%!==! goto %pass%
set pass=pass2
set count=!!
set flag=
set pattern=
set realdir=
set testdir=
set olddircmd=%dircmd%
set dircmd=
set a=
set ostype=DOS
if %OS%!==Windows_NT! set ostype=NT
set null=nul
if %ostype%==NT set null=
:loop0
set pattern=%pattern%%1
if %2!==! goto cont0
set pattern=%pattern%
shift
set flag="
goto loop0
:cont0
if not exist %flag%%pattern%%null%%flag% goto error
echo %pattern%> }1{.dat
dir %flag%%pattern%*.*%flag% /a:d | find "Volume in drive" > }2{.bat
echo set !=%%3> volume.bat
call }2{
set !=%!%:\
set realdir=%!%
:outerloop
dir %flag%%realdir%*.*%flag% /a:d /b > }3{.dat
echo.>> }3{.dat
date < }3{.dat | find "En" > }4{.dat
echo exit>> }4{.dat
command /e:1024 < }4{.dat >> nul
call }5{.bat
echo %realdir%> }6{.dat
find /i "%pattern%" }6{.dat > nul
if not errorlevel 1 goto display
set count=%count%!
goto outerloop
:pass2
if %ostype%==NT goto nt1
if %4!==! goto end
goto cont11
:nt1
if %5!==! goto end
:cont11
set a=%realdir%
set testdir=
:innerloop
if %ostype%==DOS set a=%a%%4
if %ostype%==NT set a=%a%%5
if %ostype%==DOS set testdir=%testdir%%4
if %ostype%==NT set testdir=%testdir%%5
shift
if %ostype%==NT goto nt2
if %4!==! goto cont1
goto cont12
:nt2
if %5!==! goto cont1
:cont12
set a=%a%
set testdir=%testdir%
goto innerloop
:cont1
set a=%a%\
find /i "%a%" }1{.dat > nul
if errorlevel 1 goto end
echo set %count%=%testdir%> }5{.bat
echo set realdir=%a%>> }5{.bat
goto end
:display
:cleanup
set dircmd=%olddircmd%
set maxcount=%count%
set count=!
echo The elements in %pattern% are
:cleancountloop
set %count%=
if %count%==%maxcount% goto cleancont
set count=%count%!
echo echo %%%count%%%>}5{.bat
call }5{
set %count%=
goto cleancountloop
:cleancont
set pass=
set ostype=
set count=
set maxcount=
set pattern=
set realdir=
set testdir=
set a=
set flag=
set olddircmd=
del }1{.dat
del }2{.bat
del }3{.dat
del }4{.dat
del }5{.bat
del }6{.dat
goto end
:error
echo.
echo.
echo ERROR Report: %pattern%
echo does not exist, lacks a trailing '\' or has some other error.
:end
Total clarity ... sure it is. There is a commented version (about 11Kb). You should not try to retype, or even copy/paste either one because there are significant trailing spaces in some places and other places where a trailing space is a fatal error; feel free to save the files and test them, though (the usual "no commercial use" copyright license applies). If you have a problem with too little environment space, you can say COMMAND /e:1024 at the prompt to spawn a secondary command processor with a larger environment. In some cases, you may need a larger number in the /e: switch or you can add
set pass=pass1
command /e:2048 /c%0 %1 %2 %3 %4 %5 %6 %7 %8 %9
goto end
:pass1
just ahead of the set pass=pass2 line.
There are a number of personal firsts in this program, beginning with it's name: ENTER.BAT. I've used that name before in list processing, but this is the first time I've found it necessary to use it for the master file. The program is recursive - it uses the :PASS2 code to process directory entries by redirecting the list of entries into COMMAND.COM> after passing them through DATE to prefix each line with Enter (and some other garbage that gets ignored) and therefore must be invoked when the command ENTER is given. Another first was string comparison by running the pattern and the string in work through FIND in both directions: if a is in b and b is in a, then a and b are identical. Using the /i switch to FIND makes the comparisons case insensitive. But mainly, this is the first batch program I've written that is explicitly intended to work in all three "DOS" environments.
Plain text version of ENTER.BAT and Commented ENTER.BAT are available if you wish to view them or otherwise have a problem with files with the .BAT extension.
In some cases completely different programs are required. There are a several ways to deal with this:
Detecting the OS is not a straight-forward task: VER tells us what the base OS is, but not whether we are running in a DOS box (Win3.x and Win95). Most of the time that isn't important and VER would be usable, however, to make use of VERwe have to either run it through FIND to check for key words, or import it into an environment variable - both of these require enough extra disk activity to make them undesirable if an alternative that does not require pipes is available. Unfortunately, the alternative of deducing the information from environment variables also requires pipes and FIND because some of the variables are in lower case and can be extracted only with considerable difficulty. The least inefficient procedure seems to be to look for NT first, using the OS= environment variable (very low overhead), then to look for "Windows" in the VER report if NT is not found. If it isn't NT, and "Windows" is not in the report, we will assume MSDOS. This ignores OS/2 and other non-Microsoft operating systems.
It's time for an aside:
I wrote this book not because I like the MS operating systems and their batch languages, but because I detest them - they are terrible operating systems with brain damaged scripting languages. NT is the least bad, but it's still a long way from what I would consider really usable. This book grew out of my need to find ways to get the job done despite Microsoft's best efforts to prevent me from doing whatever need doing. OS/2 is omitted because it is not a Microsoft operating system and I am not forced to deal with it.
Back to detecting the OS:
set this_os=DOS
if %OS%!==! goto try95
set this_os=NT
goto os_ok
:try95
ver | find "Wi" > nul
if not errorlevel 1 set this_os=WIN
:os_ok
will, in the majority of cases suffice to tell our code which block of commands to use. Since I know nothing about Win98 or NT5 at this time, I can only assume that they will not introduce additional breakage requiring additional testing.
Rule (0) for the following examples: Only real DOS, and DOS boxes in Win31, Win95, and NT4 are provided for, and then only in fairly simple cases
Long file names introduce several problems that are intractable: present or absent quotes, and batch file delimiters in file names are the most important. These interact: if
WHATARGS.BAT
@echo %1!%2
is given under Win95
"foo=bar"
as an argument, it reports
"foo=bar"!
but, if it is given
foo=bar
it reports
foo!bar
Similarly, it reports spaces correctly is the string is quoted, but not if it isn't.
From this simple experiment, it is clear that the proper way to handle file names is to require that long names be quoted and short names unquoted when passed as arguments. It is important to remember that real DOS barfs if file names are quoted when passed to functions such as DIR - this is the reason for passing short names (the only kind of names in real DOS) unquoted, though it makes no difference for short names and long names not containing delimiters in Win95 and NT. If it is necessary to pass long names unquoted, then code like that in the first example in this section will be necessary, along with the implicit assumption that the file names contain no commas, semicolons, equals signs, or clusters of spaces with or without other delimiters because the code can replace only delimiters it knows about in advance and that are hard coded in the program.
Rule (1) for the following examples:
Long file names are quoted when passed to a batch file as an argument; short file names are never quoted.
Let's see what applying that preceding stream-of-consciousness stuff can do when applied to some real word code.
A few days ago I responded to a user's question about extracting the base name from a long file name (unspecified OS, but I correctly deduced it was Win95 from the header of his usenet message) with this:
@echo off
if %1!==}{! goto pass2
set this=%0
md }{
set original=
:loop
set original=%original%%1
shift
if %1!==! goto cont
REM following line ends with exactly one space
set original=%original%
goto loop
:cont
copy "%original%" }{
for %%a in (}{\*.*) do %this% }{ %%a
goto end
:pass2
ren %2 *
dir }{\*.* /b > }{.dat
echo. >> }{.dat
set longname=
date < }{.dat | find "En" > }{.bat
echo :loop> enter.bat
echo set longname=%%longname%%%%4>> enter.bat
echo shift>> enter.bat
echo if %%4!==! goto end >> enter.bat
echo set longname=%%longname%% >> enter.bat
echo goto loop>> enter.bat
echo :end>> enter.bat
call }{
deltree /y }{
del }{.bat
del }{.dat
del enter.bat
set this=
echo The long name of "%original%" without the extension is "%longname%"
set original=
set longname=
:end
That code doesn't work in any other environment: real DOS doesn't like the quotes, and NT barfs on the syntax. The basic code copies the given file to an empty directory and renames all the files in that directory to * to strip the extension. That is an old DOS trick, but it doesn't work with long file names containing dots in either Win95 or NT4 - it truncates at the first dot instead of the last. I used the Win95 specific behavior of FOR to get the short name (FOR %%a in ( *.* ) returns the short names of every file in the directory - since there was only one, it returns it). It was necessary to omit the usual CALL from the DO clause in order to avoid having FOR return the new file name as well.
The standard DOS code can be modified somewhat to reduce disk usage
@echo off
md }{
rem> }{\%1
ren }{\%1 *
dir }{\*.* /b > }{.dat
echo. >> }{.dat
date < }{.dat | find "En" > }{.bat
set basename=
echo set basename=%%4> Enter.bat
call }{
deltree /y }{ > nul
for %%a in (enter.bat }{.bat }{.dat) do del %%a
echo %basename%
That provides a module for getting base names in DOS and Win3.1. It can be made to work under Win95 as well as real DOS and Win3.1 by using the FOR trick mentioned above:
@echo off
if %1!==}{! goto pass2
md }{
rem> }{\%1
for %%a in ( }{\*.* ) do %0 }{ %%a
:pass2
ren }{\%1 *
dir }{\*.* /b > }{.dat
echo. >> }{.dat
date < }{.dat | find "En" > }{.bat
set basename=
echo set basename=%%4> Enter.bat
call }{
deltree /y }{ > nul
for %%a in (enter.bat }{.bat }{.dat) do del %%a
echo %basename%
Note that DELTREE doesn't come with NT4 - the above programs fail unless it is present because it is left over from an earlier operating system to which NT4 was an upgrade, or is provided some other way (in a networked directory on a WfW or Win95 machine in the PATH, for example).
One user asked for a program that would provide a list of directories and their sizes - this provided me with an opportunity to extend the above multi-OS thinking into list processing: it required a bit of string processing and directory listings for each directory in a DIR /s listing. It brought up an additional point: you can't use syntax like echo set %%quote%%=">> enter.bat because the quote mark will inhibit the >>. For the fully commented version of this, see commented.dirsize.bat.html
@echo off
dir %1 /a:d /b /s > }{.dat
echo.>> }{.dat
date < }{.dat | find "Enter " > }{.src
echo exit>> }{.src
set arg=4
set sw=
set mark="
set e=/e:1024
if %os%!==Windows_NT! set arg=5
if %os%!==Windows_NT! set sw=/x
if %os%!==Windows_NT! set e=
echo @echo off> enter.bat
echo if %%%arg%!==! goto end>> enter.bat
echo set quote=>> enter.bat
echo set dname=>> enter.bat
echo :loop>> enter.bat
echo set dname=%%dname%%%%%arg%>> enter.bat
echo shift>> enter.bat
echo if %%%arg%!==! goto cont>> enter.bat
:: Note that there is a space following %%dname%% in the following line
echo set dname=%%dname%% >>enter.bat
echo set quote=%%mark%%>> enter.bat
echo goto loop>> enter.bat
echo :cont>>enter.bat
echo echo %%dname%% (size in bytes)>> enter.bat
echo if exist %%quote%%%%dname%%\*.*%%quote%% dir %sw% %%quote%%%%dname%%%%quote%%>> enter.bat
echo if not exist %%quote%%%%dname%%\*.*%%quote%% echo 0 file(s) 0 bytes>> enter.bat
echo :end>> enter.bat
%comspec% %e% < }{.src | find " bytes" | find /v " free">result.txt
del }{.dat
del }{.src
del enter.bat
set sw=
set arg=
set e=
set mark=
This approach to list processing is based on the LOADFIX/DATE approach analysed in List Processing but redirects the preprocessed list to the input of the command processor (COMMAND.COM (DOS and Win95) or CMD.EXE (NT4) so that the ENTER command (provided here by ENTER.BAT) is invoked once for each line in the file. Note that it is necessary to append an EXIT command to the end of the file, or the command processor will never terminate and the batch program will hang.
SWEEP [directory [file]]
where directory is any valid directory specification, including nothing at all (for the default directory) and file is any valid file specification that does not end with a backslash, including nothing at all, which turns off file scanning. Note that if a file specification is used, the directory specification must also be used because there is no way to tell the difference between a file specification and some allowed forms of directory specification - there is no way to specify files in the default directory, the default directory must be specified, though it can be specified with just a dot. Wildcards are allowed in both the directory and file specifications provided that they are valid for the operating system under which the program is run (in my test directory, *p.bat returns only SWEEP.BAT, because that is the only file having a name ending in p and a .bat extension on Win95 and NT4 systems, but all the batch files on the MSDOS 6.22 system. @echo off
if not %2!==! goto cont0
dir %1 /a:d /b /s > }{.dat
goto cont1
:cont0
if not %2!==! dir %1\%2 /b /s > }{.dat
:cont1
echo.>> }{.dat
date < }{.dat | find "Enter " > }{.src
echo exit>> }{.src
set arg=4
set sw=
set mark="
set e=/e:1024
if %os%!==Windows_NT! set arg=5
if %os%!==Windows_NT! set sw=/x
if %os%!==Windows_NT! set e=
echo @echo off> enter.bat
echo if %%%arg%!==! goto end>> enter.bat
echo set quote=>> enter.bat
echo set dname=>> enter.bat
echo :loop>> enter.bat
echo set dname=%%dname%%%%%arg%>> enter.bat
echo shift>> enter.bat
echo if %%%arg%!==! goto cont>> enter.bat
:: Note that there is a space following %%dname%% in the following line
echo set dname=%%dname%% >>enter.bat
echo set quote=%%mark%%>> enter.bat
echo goto loop>> enter.bat
echo :cont>>enter.bat
echo set thisitem=%%quote%%%%dname%%%%quote%%>> enter.bat
echo call }user{>> enter.bat
echo :end>> enter.bat
%comspec% %e% < }{.src > nul
if exist }return{.bat call }return{
if exist }return{.bat del }return{.bat
del }{.dat
del }{.src
del enter.bat
set sw=
set arg=
set e=
set mark=
While the program can be incorporated into another batch file with no difficulty, the example is designed to be CALLed from another batch program. Note that it cannot write }USER{.BAT, which contains the program to be executed once for each item in the list generated by the SWEEP program because this would require redirecting redirection characters. }USER{.BAT must be prepared separately. sweep to list all the subdirectories in the default directory.
}USER{.BAT
@echo off
echo echo This is a directory: %thisitem%>> }return{.bat
Test syntax: sweep . *.bat to list all the batch files in all the subdirectories in the default directory.
}USER{.BAT
@echo off
echo echo This is a batch file: %thisitem%>> }return{.bat
If those work, and they do, then everything else that involves only valid commands and file/directory specifications should also work. To see, let's examine a couple of real-world problems. These were taken from alt.msdos.batch on adjacent days. In the first, we will illustrate CALLing SWEEP.BAT and in the second, the code will be incorporated into the master program. Where the user specified the C: drive, I have changed the code to use my test RAMDRIVE (I'm not fool enough to test experimental code that has teeth like these on my main HDD). I created fake directory structures with dummy files on the RAMDRIVE to simulate a real-world configuration. The RAMDRIVE lives on the MSDOS/WfW3.11 system, but is accessible by all my systems, so I use it for safe testing. @echo off
if %2!==! goto end
set count=!
call sweep %1 %2
if %count%==! goto error
set last=%count%
set count=!
:loop
if %count%==%last% goto end
echo echo A copy of %2 was found as %%%count%%%> }{.bat
echo set %%count%%=>> }{.bat
call }{.bat
set count=%count%!
del }{.bat
goto loop
:error
echo %2 was not found anywhere in %1 or its subdirectories
:end
set %count%=
set count=
set last=
and this is }USER.BAT{
@echo off
echo set %%count%%=%thisitem%>> }return{.bat
echo set count=%%count%%!>> }return{.bat
The calling syntax is the same as for SWEEP: the file name followed by the directory to begin searching in and then the pattern for the file(s) to be found - if the program is FINDFILE.BAT, the root of the C: drive is the starting point, and the program is to find TEST.BAT then the syntax is findfile c: test.bat - remember to quote names with spaces in them. The path argument (the first one) must not end with a backslash.echo set %%count%%=>> }{.bat and set %count%=
The other example is in response to a code to move all files of a specific type from wherever they are on the HDD to a specific directory. One respondent suggested MOVE /s, which would be great, except that MOVE doesn't accept a /s switch. This batch program implements that functionality. @echo off
if %thisitem%!==! goto end
set flag=/y
if %os%!==Windows_NT! set flag=
move %thisitem% %target%
:end
MOVE.SLASH.S.BAT (name it something 8.3 for real DOS)
@echo off
if %3!==! goto end
set target=%3
echo %3> }}{{.dat
find " " }}{{.dat > nul
if errorlevel 1 set target="%3"
dir %1\%2 /b /s > }{.dat
echo.>> }{.dat
date < }{.dat | find "Enter " > }}{{.dat
find /i /v %target% < }}{{.dat > }{.src
echo exit>> }{.src
set arg=4
set sw=
set mark="
set e=/e:1024
if %os%!==Windows_NT! set arg=5
if %os%!==Windows_NT! set sw=/x
if %os%!==Windows_NT! set e=
echo @echo off> enter.bat
echo if %%%arg%!==! goto end>> enter.bat
echo set quote=>> enter.bat
echo set dname=>> enter.bat
echo :loop>> enter.bat
echo set dname=%%dname%%%%%arg%>> enter.bat
echo shift>> enter.bat
echo if %%%arg%!==! goto cont>> enter.bat
:: Note that there is a space following %%dname%% in the following line
echo set dname=%%dname%% >>enter.bat
echo set quote=%%mark%%>> enter.bat
echo goto loop>> enter.bat
echo :cont>> enter.bat
echo set thisitem=%%quote%%%%dname%%%%quote%%>> enter.bat
echo call }user{>> enter.bat
echo :end>> enter.bat
%comspec% %e% < }{.src > nul
del }{.dat
del }{.src
del enter.bat
set sw=
set arg=
set e=
set mark=
:end
In case you think this stuff is easy to write, think again - the material from SWEEP to here represents about eight hours of work. Much of it trying to find commands that will work in all three operating systems. On example is the approach to removing the target directory in the last example: putting the FIND in }USER{.BAT simply didn't work - the idea was to put the THISITEM data in a file and compare that to TARGET using FIND, then test the ERRORLEVEL - worked fine in Win95 and real DOS, but prevented NT4 from processing more than the first line in the list. There were many other things like that. This sort of multiple OS batch code must be considered fragile.
@echo off
set field=3
set key=Current
if %OS%!==Windows_NT! set field=4
if %OS%!==Windows_NT! set key=The
echo. | date | find "%key%" > }{.bat
echo shift> %key%.bat
echo set xdate=%%%field% >> %key%.bat
call }{
echo. | time | find "%key%" > }{.bat
echo set xtime=%%%field% > %key%.bat
call }{
del %key%.bat
del }{.bat
set field=
set key=
echo %xdate% %xtime%
This approach is probably language version specific - that code is for the US English versions (all I have access to).** Copyright 1998, 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