NT Specific Batch Programs and One Liners
One-liners from the command line
These are single line commands that do fairly complex tasks in NT that would be difficult or impractical in any other batch language version.
Note: while I talk about batch files, and use .bat extensions on the test programs, there is a difference between .bat and .cmd files. There doesn't appear to be any difference until you use a shortcut to launch the program, then .bat files get COMMAND.COM as their command processor, and so are running in an environment similar to DOS 5 with no NT extensions, but if the extension is .cmd, then the shortcut launches the program with CMD.EXE and all the NT extensions are present and functional.
for /r n:\gearbox %a in (*.*) do if exist \%~na%~xa del n:\%~na%~xa
This cleaned up most of the mess left when a restore from tape dumped all the files into the root of the drive and then the restore was repeated more correctly. For each file in the directory tree, there should be a file in the root, but not every file in the root was put there by accident. That command walks the restored directory, isolates the name (+extension) of each file in that branch, then if the file also exists in the root, it is deleted from the root.
That left 170 files in the root, most of which were part of the accidental dump, but had not been restored at all on the second attempt. Most of them appeared to be from a specific subdirectory branch that was really a backup of the corresponding branch on another machine.
for /r i:\httpd %a in (*.*) do if not exist n:\gearbox%~pa%~na%~xa if exist n:\%~na%~xa move n:\%~na%~xa n:%~pa
matches filenames in the root of the n: drive against filenames in the i:\httpd branch and if they are not already present in their proper place in the n:\gearbox\httpd branch, they are moved to where they belong. Between these two commands, an 8000+ file mistake was reduced to just 13 files to sort out by hand (most belonged in n:\gearbox itself).
In the above, and in other FOR commands and command line arguments, %~na is the name of the file - actually it is everything between the final '\' and the last dot in the file (or if the pattern is (.), directory), exclusive (it does not begin with a backslash nor end with a dot); %~pa is the path - the part of %a between the colon and the final backslash, inclusive (for directories, it is the path to the directory, not the path to the contents of the directory), and begins and ends with backslashes; and %~xa is the extension - everything from the last dot to the end of the %a string, inclusive.
A curious property of %0 (the command that launched the batch program) is that while it may contain only the base name of the file ("test" might be all that was used to launch c:\myfiles\test.bat), the %~ elements are all present: %~dpnx0 always resolves to the fully qualified filespec. This can be very useful for forcing a program into its own directory with:
cd /d %~dp0
or making the default directory spec available for refering to files created there by refering to them as %~dp0foo, where foo is the file's name.
The NT4 batch language in CMD.EXE is a refreshing change from the brain dead language of COMMAND.COM in Real DOS, and the even worse version in Win9x. There are many significant improvements, from IPCONFIG writing network data to STDOUT, to the ability to redirect STDERR, to the marvelous enhancements to FOR and to batch command line arguments. Much of this was broken in the initial release, but was fixed in the service packs.
Let's begin with an example - a real-world program - that selectively backs up files and directories from the C: and D: drives to the F: drive. This is the real documentation for a real program in actual use by some of my users.
One interesting bit of CMD.EXE breakage came to light when I tested the commented version of the program: it has been said that CMD.EXE completely ignores lines beginning with REM - this is not true. This line breaks the program
REM possible to use "%target%%~pnxa" in the FOR loops, but this seems more generic.
because CMD interprets "%target%%~pnxa" and barfs on it.
This one is from a response to a question in alt.msdos.batch, which I cross posted to alt.msdos.batch.nt where it really belonged. Someone else posted a shorter solution (shorter mostly because of highly condensed code) - with no explanation - that didn't work. The original question was
"How do I rename 2000 files so that the file name increments
eg. file1.txt would become 0001.txt
file2.txt would become 0002.txt and so on?"
To which I replied with the obvious questions.
Q. What operating system do you want this for?
A. Windows NT
Q. Are all the files in the same directory?
A. Yes
Q. Is there some order to the files?
A. Yes Alphabetical (but this is not a must)
Q. Or can they be assigned random numbers as long as no numbers are omitted?
A. No I need them to be numbered incrementally
Q. Just what *are* you trying to accomplish?
A. The impossible
Well, it's not impossible in NT. Ordinarily I would have suggested using a secondary language to process do all the work, but NT's FOR, IF, and SET commands are enough of a mini-language in themselves to do the work.
The task is "file1.txt would become 0001.txt" and so forth, which indicates the need for leading zeros, which does add a bit of complexity.
RenameAsNumbers.bat @echo off
if %1!==! goto end
if %1!==}{! goto pass2
dir %1\*.* /b /a:-d /o:n /s > %temp%\}{.dat
set count=0
for /F "tokens=*" %%a in (%temp%\}{.dat) do call %0 }{ "%%a"
del %temp%\}{.dat
goto end
:pass2
set /a count+=1
set fname=%count%.txt
if %count% LSS 1000 set fname=0%count%.txt
if %count% LSS 100 set fname=00%count%.txt
if %count% LSS 10 set fname=000%count%.txt
ren %2 %fname%
:end
Explanation:
Since this program renames *all* the files in a directory, it cannot itself exist in that directory - it must be passed the name of the directory to work on as an argument, and it must be quoted if any spaces appear in the directory name ... best always to quote it. If this argument is missing, the program just aborts.
In order to keep the entire program in one file, the program is recursive for the action of the FOR command - the syntax used is well known and is documented at .
The DIR command has a GOTCHA - watch out for this - in order to make DIR /b return the complete filespec, it is necessary to also use the /s switch. This means that not only all files in the given directory will be renamed, but also all those in all its subdirectories. I could have included a test for subs, but I didn't, so it is *ESSENTIAL* that the target directory not have subdirectories. The other switches ensure that only files will be listed and that they will be in alphabetical order.
A variable named "count" is used to keep track of the numbers - since it is incremented before use and we want the first file to be 0001, we initialize the variable to 0. This variable does not have the leading zeros.
The FOR command opens the file created by the DIR command and passes each line (note the quotes around %%a) as a quoted string as the second argument to the recursive call to the program. That is, the program itself is called with the recursion marker and the quoted string as arguments.
For each line in the file, the :pass2 code is executed. This code increments the counter; adds leading zeros as appropriate (and the extension) to create the new name, then renames the file. Since there are no spaces in the new names, no quotes are needed there, only in the original name.
After completion of the last FOR, the program deletes the transient DIR listing file and jumps to the end to terminate.
Another way to do this is to use FOR on the directory, but this requires that the target directory be the default - there are always trade offs.
A user asked about taking a list of strings from one file and writing each one to a file named on the same numbered line in a second file. Ordinarily this would be done by saving each string in an array indexed by line numbers, then reading the filenames file and using the line number in that file as the index to retrieve the string to write to the file named in that line. This requires the concepts of line numbers as arrays, neither of which is explicitly available in NT's batch language.
Line numbers can be added to lines with FIND /n
find /n /v "" < source > target
where /v and "" cause FIND to output all non-null lines, and /n causes each line to be prefixed with the line number between []s (no delimiter between the closing ']' and the beginning of the line.
FOR /f "tokens=1,2* delims=][" %%a in etc.
splits the line in work into pieces, placing the line number in %%a and the string in %%b.
Arrays can be simulated in the environment by using variable names containing numbers, and therefore, the above FOR command can be used to set a numbered sequence of variables to the strings in the file.
The resulting batch file is
@echo off
if %1!==}{! goto %2
find /v /n "" < %1 > %temp%\}first{.tmp
find /v /n "" < %2 > %temp%\}second{.tmp
%comspec% /c%0 }{ pass2
del %temp%\}{.bat
del %temp%\}first{.tmp
del %temp%\}second{.tmp
goto end
:pass2
for /f "tokens=1, 2* delims=][" %%a in ( %temp%\}first{.tmp ) do set xx%%a=%%b
for /f "tokens=1, 2* delims=][" %%a in ( %temp%\}second{.tmp) do (set xxx=%%b && call :pass3 %%a)
goto end
:pass3
echo echo %%xx%1%%^> %xxx% > %temp%\}{.bat
call %temp%\}{.bat
:end
where the first argument is the strings file and the second is the filenames file.
A misfeature, wart, or bug - since it is the sort of thing to cause programmers to have nightmares, it's probably a screw - of FOR /f is that if a field is null, it is not counted, that is, while the delims string clearly states that '[' and ']' are delimiters, and the line begins with a delimiter, we want to use the null first field and the rest of the line, reality is that the second field, the number, is returned as the first field. (gawk correctly returns the number as the second field, but it mishandles consecutive field separators that are declared explicitly. None of this is formally documented for either language, though use of the word "token" to refer to the fields does imply that only logical elements are of interest, that is, the line is parsed into symantic units rather than split into literal fields (the latter is more useful to batch programmers).)
The batch program has several points of interest
- use of FIND to prefix the lines with line numbers and FOR /f to extract the number and string separately
- running the code that modifies the environment in a secondary shell so that when the program terminates, the temporary variables used as the array will simply go away and need not be cleared explicitly (this removes the need for the program to know what numbers where actually used).
- use of variable names composed of a fixed string and a variable number to simulate the name and index of an array
- multiple recursion
- once through a secondary command shell
- once through a call to an internal label (one of the things that makes the program NT specific)
- use of '^' to escape '>' in an ECHO command (one of the really useful NT specific features) - in other MS batch languages, it is very difficult to get a string containing a redirection operator into a file (usually a PROMPT string is used with a secondary (tertiary) command processor).
- use of a secondary batch file to resolve the composite variable name into its value.
There is no argument sanity testing, but it would be easy enough to add testing to make sure that there were exactly two arguments (if %2!==! goto error, and if not %3!==! goto error), and that both exist as files (if not exist %1 goto error, and the same thing for %2).
Step by step - comments follow the command they refer to
- @echo off
- Suppress display of the commands in the file
- if %1!==}{! goto %2
- First level of recursion: if the program was invoked with the recursion marker and the name of a label, jump to that label
- find /v /n "" < %1 > %temp%\}first{.tmp
- find /v /n "" < %2 > %temp%\}second{.tmp
- Run the two files through FIND to prefix all non-null lines with line numbers. Note that the number will for a monotonic sequence, but if there are any blank lines in the file, the sequence will not be continuous
- %comspec% /c%0 }{ pass2
- First level of recursion: invoke a new command processor (CMD.EXE by default) and restrart the program, this time with the recursion marker and target label as arguments (the real arguments are not needed since the names of the copies of the files are known)
- del %temp%\}{.bat
- del %temp%\}first{.tmp
- del %temp%\}second{.tmp
- goto end
- Those lines don't exceute until after the program has done its mail work - they clean up the transient files left by the main part of the program; note that this code is back in the original command processor
- :pass2
- The jump target for the first level of recursion; everything brom here to the next "goto end" command runs in the secondary shell
- for /f "tokens=1, 2* delims=][" %%a in ( %temp%\}first{.tmp ) do set xx%%a=%%b
- That line
- reads the strings file one line at a time,
- splits the line up into fields using ']' and '[' as field delimiters (see parenthetical comments above about it really being parsed into symantic tokens)
- uses the second field (first token - the number) as part of the otherwise arbitrary name of a variable to be assigned the rest of the line as its value ("2*" defines the rest of the line as the second token)
- repeats for the next line in the in the file until the file is exhausted
- for /f "tokens=1, 2* delims=][" %%a in ( %temp%\}second{.tmp) do (set xxx=%%b && call :pass3 %%a)
- Does the same sort of processing as the line above, except that there are two actions in the DO clause:
- a variable having a known name is set to the filename contained in the line (as the line)
- a subroutine in the program is recursively called and passed the line number as its only parameter - this appears as %1 when the subroutine executes
- goto end
- This marks the end of the first level of recursion. The code in this block plus the code called from this block (pass3) all executes in the secondary command processor.
- :pass3
- The entry point for the subroutine to be called once for each non-null line in the original filenames file (every line in the filtered file).
-
echo echo %%xx%1%%^> %xxx% > %temp%\}{.bat
- Creates a batch file containing a command of the form
- echo %xx123% > filename
- where %xx123% resolves to the string stored in the 123rd location in the pseudoarray
that writes the string to the filename
- call %temp%\}{.bat
- Invokes the batch file to execute the command to write the string to the file
- :end
- Target of all those "goto end" commands; the end of the file, first level of recursion, subroutine, and program.
** Copyright 2000, 2001 Ted Davis - see License, included by reference. **
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