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 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).



  ** 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