We have few tools for debugging batch files, and those we do have are very crude by the standards of regular programming languages. We have ^C, which will sometimes stop a batch file; we have PAUSE, which we can add after some or all lines; we have COMMAND /cfoo > bar; and in DOS 6, we have the /y switch to COMMAND, which is a more convenient equivalent of adding PAUSE before every line, but does require a secondary command processor. All except the last need to have the ECHO OFF command commented out.
For very small, very simple, batch files, we can just comment out the @ECHO OFF and see what happens, but this breaks down rather quickly if the structure contains loops or is reentrant.
PAUSE stops processing and prompts for any key. If ECHO is off, this lets us see a few of the most recent commands (with the variables expanded) and any output from those commands. A reader suggested making the PAUSE command a variable so that they can be turned off and on with an environment variable. Of course, we could make the commands conditional (if !%debug%==!true pause), but simply making a command line
%debug%
and setting DEBUG to "pause" either at the command prompt or in a wrapper batch file is more compact. It also has the advantage that we don't really have to decide what command it is to be until run time. It has the disadvantage that setting it for use with one batch file, and then running another before deleting the setting can have unexpected effects on the second program. My approach to this is to use something like %foodebug% where foo is the name of the program or part of the name. Note that you cannot use %0 inside the variable name (that would be an obvious way to incorporate the name without having to know it at coding time, but only one level of variable reference can be used).
For some years, I relied on
COMMAND /cfoo > bar
(where foo is the batch file and bar is the file into which I want to redirect
the output of the test) and an AWK command that added PAUSE in
front of every line. I always do my development with the file in work named
TEST.BAT and, if there are several in work at one time, the individual
TEST.BATs are in directories named what the final file will be named. The
following ADDPAUSE.BAT file is in c:\bin, which is in the path, as is a
free version of AWK (specifically MAWK, from the DOS under
UNIX library of UNIXFORUM on Compuserve). Note that I
have to comment out the
@ECHO OFF
line and make sure that the last line has a CRLF at its end, both of
which must be fixed when cleaning up the program for release.
@awk "{print \"pause\"; print $0}" test.bat > testp.bat
This is now obsolete, except in special cases, thanks to the /y switch for COMMAND.COM - TESTY handles this:
@command /e:1024 /y /ctest
In some cases, I need a permanent record of the action, and for that I run the batch file in a secondary command processor with its output redirected to a file - again I am very consistent with my file names, so RUN2ND.BAT can be used to automate the syntax:
@command /e:1024 /ctest > dump.lst
Note that there are three acceptable responses to the Y/N prompt: Y, N, and ^C - the latter aborts the program (useful when you see the problem and really don't want to have to step through the rest of the file).
The
/e:1024
switch ensures that the batch file will have plenty of environment to work with.
If the program loops, ^C generally stops it. Examination of the dump file
allows tracing the action, step by step and line by line.
Note that in all these cases where
ECHO OFF
is deactivated, the lines that are actually displayed are those that will
actually be executed, with all macros and variables expanded.
When I am having problems with batch files that use environment variables, I usually set up a CLEANUP.BAT file in the same directory that contains the cleanup line(s) from the file in test, so that I can comment out the clean up code and examine the environment and temporary files after the file in test has terminated or has been aborted, and then clean it up in preparation for the next pass. CLEANUP.BAT clears all the environment variables used by the program and erases all it's temporary files.
I have also found it helpful to work in a DOS window under Windows (so that, if all else fails, I can abort the program by using the "three finger salute" (Ctrl-Alt-Del) to kill the DOS window), and to point the temporary files to a RAMDRIVE. I also work out of the RAMDRIVE and keep a copy of the latest stable stage of the work on the HDD (so that if something goes terribly wrong, rebooting will remove all the likely damage, and set me back to the last stable state).
I have said that I always work in a certain way, using specific procedures and file names - that's not exactly true, and the exceptions tend to get me in trouble, so do as I say do, not as I do. The most recent example is from a couple of days ago:
A week or two ago, I got clobbered when the standard batch
file method of detecting whether two files are identical failed due to the
nature of the files and I used my regular approach to build a batch file test
that didn't fail that way, but had a large gotcha, which I found when I tested
it over a wide range of inputs - then the general question was posted and the
standard answer was given, and I replied with the fix. *But* ... the fix was at
work and I was at home - I was tired and rushed, so I cut corners - I did a
quick knockoff using the regular procedure, copied the file to the archive,
realized that I had left out the fix for the problem with the fix, modified the
copy in the archive, tested it in situ ... then posted the unfixed copy from the
development directory ... oops! My standard procedure for that is to copy the
archive copy back to the development directory (which I should have cleaned out
after archiving the file), erase the archive copy, repair the development copy,
test it in the safe directory, then copy it back to the archive,
DEL *.*
the development directory, and post the archive copy.
Another exception, this one valid,
to my standard procedure is when the batch file is potentially so destructive
that it must be tested on a machine that can be recovered easily - an example
would be any batch file using FDISK, FORMAT, SYS, or any
other utility that can trash the HDD. I would also be wise to test those using
DEL *.*
and DELTREE on such an expendable machine -certainly not on my main
system. If you don't have the luxury of having a test machine, the best
insurance is a good set of current, tested, backups and a set of
tested recovery disks that include the software required to restore the
backups as well as to rebuild the HDD and operating system.
The names given to labels and variables can make large differences in the ease of debugging - labels must be no more than eight characters, and it is good practice to conserve environment space by keeping variable names short, but they should not be so short as to compromise clarity. Labels and variable names should also be clearly unique, but also clearly members of the sets to which they belong - for example, a subroutine can be clearly delimited with labels such as
:foo
foo code
:fooend
but if the code contains a jump to a
label outside the function, as for a return from a recursive call to the batch
file, the obvious labels are
:end
and
:return,
but
:return
should not be used for the label at the end of the main program. If the jump is
to the beginning of another subroutine, the jump target should be the one that
begins the new routine. These somewhat conflicting requirements can be met
during development by the use of multiple labels at the same logical place in
the code, and cleaned up for the release version by replacing the jumps to
routine end labels with jumps to the adjacent begin labels and converting jumps
to
:return
into jumps to
:end
as here:
:foo
foo code
goto return
:fooend
:bar
bar code
:barend
:return
:end
becomes
:foo
foo code
goto end
:bar
bar code
:end
I make it a point to avoid having
more than one label beginning with
:end,
though long ago I used to use "end", "endd", "enddd", etc. - but I found that I
tended to confuse them. My current practice is to use "end" only at the end of
the program, especially when it is used to implement a return from a function
call, and "done" or "foodone" or "fooend", where foo is the name of the function
needing an end label, though usually this can be avoided by considering the
label to be the beginning of something else rather than the end of something. My
practice in this area is still changing, so you may see the occasional "endfoo",
but I know it isn't the best possible practice.