Batch Script Hygiene

Today I want to share a companion to the PowerShell Script Hygiene post from a while back.

Isolating changes

The first thing you'll want to do with most scripts is to avoid having changes made to your environment leak out to the caller.

Two classic things to get wrong are changing environment variables, or changing the current directory.

Both of these things can be prevented by using setlocal. When the script ends or if you call endlocal, environment variables and the current directory are restored.

Two useful things to include as well are ENABLEEXTENSIONS

setlocal ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION

Enabling delayed expansion is particularly useful with loop commands. Read more about it here.

Error handling

The typical way of checking for errors in batch files is to test the errorlevel value using if.

There are a couple of ways of doing that:

The first form tests that errorlevel is 1 or greater. Most commands return 0 on success, so this usually works.

There are two exceptions, however, which is why I often prefer the second form. The first is that many programs propagate HRESULT or similar values out into the program exit code. This is unfortunate because they are negative integer values, and so they fail the "is greater than or equal to one" test.

The second is programs like fc (file compare), which uses the result value to indicate whether the files match, don't match, or whether an error was found. So "one or greater" isn't always an error. robocopy is another commonly used program where non-zero might not mean a failure.

Once you've determined that things went wrong, you'll commonly want to stop running the batch commands. To do that, the exit command should be used with the /b command and an error code. This will cause execution to return to the caller, rather than exit the command prompt.

Finally, if you want to jump to a cleanup section, you can use the goto command with a label. Labels should be unique within the file and start with a semicolon.

All put together, this is how a error handling might look like.

run_my_command
if not "%errorlevel%" == "0" (
  goto :fail
)

echo success!
exit /b 0

:fail
echo failure
exit /b 1

Argument handling

To handle arguments to your batch file, you will typically want to start by keeping them simple. Batch files have very limited string handling facilities, and the command line parser can often get in the way.

Argument come in as variables in the form %n, where n=0 is the batch itself, and n=1 is the first argument. If you refer to non-existent variables, they show up as blank strings.

If you can require positional parameters, this might be all you need. You can test for the presence of the parameters with a simple if "%1"=="" ( goto :missing_param )

It's often handy to support help by starting off with something like so:

if "%1"=="/?" goto :showhelp
if "%1"=="-?" goto :showhelp
if "%1"=="-help" goto :showhelp
if "%1"=="--help" goto :showhelp

setlocal ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION
rem your script comes here

rem success!
exit /b 0

:showhelp
echo Description of your program goes here
echo.
echo foo.bat arg1 arg2
echo.
echo   arg1 - ....
echo   arg2 - ....
echo.
exit /b 1

If you instead have variable arguments, you can often process them in a loop by refering to the first variable one and shifting the parameters into that position. The shift command will do that. By default it will affect all arguments exception %*, which contains all the arguments in a single string (except for %0, which is the batch name).

If you want to keep %0 as the batch, you can use shift /n 1.

This is also what you would use to support parameters beyond %9.

If you end up building lists of arguments in a loop of sorts or within an ( ... ) block, you'll find the delayed expansion useful. Otherwise your set commands may surprise you when they seem to "not stick."

You can use the same substitutions as those available to for variables.

Another very useful thing you'll find when building batch files is getting a reference to the directory where the batch is. This allows you to invoke the script from anywhere and results that are independent of the current directory. You would do this with set scriptdir=%~dp0

If you assign to an environment variable, you can use the full power of substitutions.

Logging

There are two approaches to log from batch files - an explicit log file you write to, or simply writing to output.

In both cases, you'll use echo.

By default, the batch file will print out each command. If this is too distracting, you can turn off this with @echo off. The leading @ can be used with any command to suppress the echo. In this case, we don't want the echo off command itself to be printed.

Composing

Once you start building more interesting things, you'll want to reuse bits of code, or organize it in blocks.

Your friend here is the call command.

You use call to invoke a new batch file (or a label in your current batch), and then return control. This makes it work like a subroutine, including taking arguments.

If you don't use call with a batch file, control simply transfers. It's like using a goto with a label.

And, of course, if this isn't powerful enough, you can always use PowerShell!

More Examples

For a fairly comprehensive example, see the hctbuild.cmd script in the DirectX Share Compiler project. It shows parsing variable arguments, error handling, help usage, and a few other interesting techniques.

Happy batching!

Tags:  shell

Home