Wednesday, October 20, 2010

Windows batch files

Introduction

In general they suck. They started off weak and even though they have improved with each release of Windows this just leaves you with a backwards-compatibility problem.

On the other hand, they are built in to Windows. Using something else introduces a dependency on a third party tool. If you want to provide a simple example of how to call your program from a command line, or want to save a command and its parameters and run it by double-clicking or from a scheduled task then a batch file will do.

One big problem used to be that the current folder defaulted to the Windows folder when the batch file is executed by double-clicking it, leaving you with the problem of finding where your stuff is. Installing all your files into the system folder or putting everything in the path is generally not workable.

Here’s a simple example batch file that will set the current folder to be the folder the batch file is in, which will, presumably, also be the one your executables are in. Paste it into a text file and save it as example.bat. You can run it by double-clicking it in Windows Explorer or by typing the filename into the command prompt and pressing Enter.

You can get help for most of the commands by typing help followed by the command name at the command prompt.

Example batch file

@rem Switch echo off
@echo off

rem Fixing current folder

echo Current folder at start: %cd%

set ARG0=%0%
echo Argument 0: %ARG0%

echo Drive: %~d0%
echo Path:  %~p0%

if %~d0==\\ goto UncNotSupported

pushd %~dp0%
echo Current folder now: %cd%
popd

echo Current folder at end: %cd%

goto end

:UncNotSupported
echo UNC paths not supported - try mapping a drive with "net use"
goto end

:end
if "%1%"=="" pause

Deconstruction

By default, each statement is echoed to the output before it is executed. This can be very handy for debugging a batch file (especially as variables don’t always work the way you might expect), but makes the output harder to follow.

Starting a line with @ prevents the line from being echoed. It’s just interpreted and executed. The first line is a comment, rem being short for remark, so it doesn’t do anything. It would still be echoed to the output so the line starts with @ to prevent this.

The second line of the script uses the echo command to turn echoing off for the rest of the script. This line would also be echoed so starts with @. Once echo is off we can dispense with starting each line with @.

You might want to switch echo back on and off again for sections of a batch file when debugging because the echoed output will display the values that variables, etc, have expanded to.

The next line is a straightforward comment.

Then I use the echo command in a new way: to write something to the output for the user to read. I write out a message and the value of the current folder, which I get from the cd pseudo-variable. The % symbol is used to enclose variable names. Other commands may work with just an initial %, but I usually enclose them anyway (if possible) to be consistent. The inconsistency around variable expansion is just part of the suck of batch files.

Next I create a variable, called ARG0, with the set command, and then I output the value. Batch files can be fussy about the set command and won’t work if you try to use spaces around the =. The variable called 1 expands to the value of the first parameter passed to the batch file (if any) and 2 is the second parameter, etc. The 0 (zero) variable is automatically set to be the full path (and file name) of the batch file itself. Putting this into another variable is redundant, but demonstrates how to set a variable.

The next two lines use an extended version of the variable expansion to output the drive letter and path from %0%. See the for command for details (help for from the command line).

The next part of the suck is that the command line cannot have its current folder set to a UNC (Unified Naming Convention) path (a share on a server such as \\server\share). If you try to run this batch file from a share then it won’t work. You can map a drive letter to the share and then run it as normal.

The next line detects UNC paths, demonstrating the if command. If the batch file is running from a UNC path then I use a goto to jump to a label. While goto is generally considered to be a bad thing, the flow-of-control support in batch files is so sucky I generally avoid it and stick to using goto.

If you really, really need to have the batch file run via a UNC path then it might be possible to figure out the share’s path from %0% and then build fully-qualified paths to all files referenced from the batch file. I’ve not tried this though.

The pushd command changes the current folder to be the folder the batch file is stored in by using a combined form of the variable expansion we saw earlier. This evaluates to the drive and path, lopping off the batch file’s name. pushd will also save the previous current folder onto a stack and this can be restored via popd. This is useful because if your batch file is called from another batch file then you need to put the old current folder back before you finish or you risk breaking the calling batch file.

Any payload for a real batch file would go between the pushd and the popd. In this case I just output the new current folder to show it worked.

I use a goto to skip over any other labelled blocks, such as the one I used to handle UNC paths, and exit the batch file. Before we exit it’s nice to use pause so the user can see what we did. If we don’t pause and the batch file was launched via a double-click then it just vanishes at the end. If the batch file is called from another batch file then the pause isn’t necessary so I check the value of the first parameter passed to the batch file. If it is empty then I pause. If called from another batch file then anything can be passed as a parameter to the batch file to have the pause skipped.

Conclusion

So, what have I achieved here? Not a lot really. This batch file is an example which demonstrates some simple techniques (formatting output, getting and setting variables, conditional jumps, etc) and works-around a problem with current folders which isn’t even relevant on Windows 7 & Windows Server 2003 (at least) as they seem to change the folder automatically anyway.

Writing to files (logging, etc)

This stuff isn’t in the example above, but might also be useful.

echo blah blah > example.log

The above writes the output to a file, overwriting it if it already exists. Using >> would append it instead.

type example.log | more

The above should output the file, but instead of writing directly to the output it should be sent to the more command as input. more just sends it to the output anyway, but introduces a pause after each page so the user can read a file a page at a time. Completely obsolete, but in theory this ‘piping’ could be used to plug all sorts of little commands and batch files together. Not tried it myself. Sounds painful.

No comments: