/ MS-DOS

Batch Scripts Can Not Suck

Note, I am assuming you are familiar with Microsoft Windows, possibly batch scripting, or at least have some interest in the topic. If you didn't know, the world is rife with batch scripts that suck. Common side-effects of batch scripts that suck may include: difficulty reading, loss of maintainability, and premature balding. In this post we will look at 5 ways to help batch scripts not suck. Here is the short (TL/DR) version for those who are short on time:

  1. Add comments
  2. Use variables
  3. Use functions
  4. Centralize configuration
  5. Prompt before execution

People still say "DOS batch scripting" but I am just talking about batch scripting that takes place in Microsoft Windows when using the command shell cmd.exe.

For a long time MS-DOS was present but running in the background behind Windows as part of the Windows bootstrap process. For most of us using Microsoft Windows, DOS is dead but you may still prefer the idea of DOS. You may also sometimes feel lonely... but fear not you are certainly not alone.

Note: I assume that you are using a modern version of Windows. Any tips I provide below should work on Windows 7+ and Windows Server 2008+ (and probably older versions too) though I am only testing on Windows 8.1. If you find an example that doesn't work for you, feel free to drop me a note with some details and I'll add some errata here for the rest of the world.

Tip #1 - Add Comments

Batch scripts can be commented using rem or :: at the start of a comment. I favor the second approach because its a better signal to me at the start of a comment. Take your pick but whatever style of comments you decide to go with, try to be consistent.

::----------------------------------------------------------------  
:: Step 1: Add some comments to your code.  
::----------------------------------------------------------------  
call :update_timestamps 

Comments are like road signs in a city, but for your code instead.

  1. They provide useful bits information and warnings along the way
  2. They sometimes help us understand the big picture

NB: Comments can be a double-edged sword. Good comments are great. It is also true that inaccurate, stale or misplaced comments can actually be worse. Take this incorrect (very bad) comment, for example:

:: ---------------------------------------------------------------    
:: Create the directory if it does not already exist.    
:: ---------------------------------------------------------------    
del /f /q *.*  

Tip #2 - Use Variables

You can create variables in your batch script by using the syntax of set <variable>=<value> and then you can refer to your variables throughout your script using %<variable>%, for example:

:: ---------------------------------------------------------------  
:: Setup variables that will be used throughout the script.  
:: ---------------------------------------------------------------  
set curr_ver=1.0  
set home_dir=c:\SomeFolder\%curr_ver%

call %home_dir%\lastchance.bat %1

Variables are a great way to make your code easier to maintain. They also reduce clutter and often add self-documenting features to your scripts. For example, using a variable such as %error_file_path% is both descriptive (the variable name implies what it should contain) and it is also preferred over hardcoding the same value in multiple places.

Starting to use variables early in the scripting process will pay off greatly, especially when we get to the part about centralizing your configuration.

It's true you could just hard code text in your scripts but then you would be missing the benefits described above and you would also have to manually or semi-automatically find/replace all instances of repeated text. NB: it is also true that %NOT% %EVERYTHING% %NEEDS% %TO% %BE% %A% %VARIABLE% but you should look for opportunities to replace repetitive values.

Tip #3 - Use Functions

Functions are reusable. They are like variables, but they can store references to fragments of code to be executed. This is nice when you want to take a block of code and use it multiple times throughout a batch script. Functions in batch scripts can accept parameters and they are referenced within the body of your function as %1, %2, Etc.

To define a function you simply need to create a :label in your code such as :my_function and then use the call command to execute it such as call :my_function. Note: you also need to include a goto reference to the EOF label (goto :eof) at the end of your function so that control returns to the point from where it was called.

For the longest time I didn't even realize batch scripts supported the use of functions. The day I found out I shed a single man tear for all the spaghetti-code I wrote using :labels and goto statements to "jump" around.

Here is an example of a function called repeat_after_me that I am calling four times in a row to do something useless. You would, of course, write some useful code in your own functions.

Example Calling Function in Batch

Here is the code I wrote for this example:

:: ---------------------------------------------------------------  
:: NAME:  
::    dos_batch_functions.bat  
::  
:: DESCRIPTION:  
::   This batch script provides an example using functions. It  
::   is a simple example and just scratches the surface.  
:: ---------------------------------------------------------------  
@echo off

:: ---------------------------------------------------------------  
:: Call a function to echo some text.  
:: ---------------------------------------------------------------  
call :repeat_after_me "Hello, World 1"  
call :repeat_after_me "Hello, World 2"  
call :repeat_after_me "Hello, World 3"  
call :repeat_after_me "Hello, World 4"

::  
:: Use 'exit' or 'goto :eof' to skip to the end.  
::  
goto :eof

:: ---------------------------------------------------------------  
:: !!!! Functions defined below this point !!!!  
:: ---------------------------------------------------------------

:repeat_after_me
 echo %1
 pause
 goto :eof

Tip #4 - Centralize Configuration

This tip is all about architecture so do this optionally and based on your own judgement. If you have a lot of batch scripts in your environment and you need to manage common variables within those scripts you can really improve maintenance of said scripts in two ways.

  1. Create Windows environment variables
  2. Create a batch script that contains only common variables

The first option is useful but not always practical and you may not want to clutter up your server / Windows environment with a bunch of process specific variables. Lets assume you are not going to use environment variables. That leaves us with the second option. So maybe we create a batch file called setEnv.bat like this:

:: ---------------------------------------------------------------    
:: !!!! Define Global Variables Here !!!!    
:: ---------------------------------------------------------------

set location=DEV

set home_dir=c:\SomeDir\1.0

set log_dir=%home_dir%\automation\log  
set err_dir=%home_dir%\automation\err  
set cfg_dir=%home_dir%\automation\cfg  
set trg_dir=%home_dir%\automation\trg

set cur_yy=!date:~10,4!  
set cur_mm=!date:~4,2!  
set cur_dd=!date:~7,2!  
set cur_h=!time:~0,2!  
if "!time:~0,1!"==" " set cur_h=0!cur_h:~1,1!  
set cur_m=!time:~3,2!  
set cur_s=!time:~6,2!  
set timestamp1=!cur_yy!.!cur_mm!.!cur_dd!  
set timestamp2=!cur_h!.!cur_m!.!cur_s!  
set timestamp3=!timestamp1!_!timestamp2!

This has the added benefit of making your code migrations easier. If you have environment specific configuration values (think DEV, QA, and PRD) then you can isolate these from your scripts by including them in a global configuration file such as the one described above.

Finally, from within all my other batch scripts I simply call this batch script to "include" all of these variables at runtime. Note the use of %log_dir% below which came from the setEnv.bat script.

:: ---------------------------------------------------------------  
:: NAME:  
::    dos_batch_config.bat  
::  
:: DESCRIPTION:  
::   This batch script is setup to show global configuration.  
:: ---------------------------------------------------------------  
@echo off

:: ---------------------------------------------------------------  
:: Import global configuration.  
:: ---------------------------------------------------------------  
call %~dp0\setEnv.bat


:: do something useful.  
call :log "Adding some text to the log file."  
:: do something else useful.

::  
:: Use 'exit' or 'goto :eof' to skip to the end.  
::  
goto :eof

:: ---------------------------------------------------------------  
:: !!!! Functions defined below this point !!!!  
:: ---------------------------------------------------------------

:log
 echo %~1 >> %log_dir%\%~n0.log
 goto :eof

Tip #5 - Prompt Before Execution

Example Prompt

This last tip is about usability and prevention of accidental execution. If you work in an environment where you often need to run batch scripts manually then you may have experienced the dreaded accidental execution. It goes something like this:

  1. Log into production (perhaps over RDP)
  2. Open the automation folder in Windows Explorer
  3. Double-click on a script
  4. Uh-oh, you just ran the wrong script and it nuked production

As shown in the screenshot above, you may want to include a safety net in some (or all) of your batch scripts to prevent accidental execution. You can do this in a way that allows the scripts to pause for a manual execution and to run without pausing if scheduled from a job or task scheduler. There are many possible ways to achieve this but I've been using this same one for years. Here is an example:

:: ---------------------------------------------------------------  
:: This script maintains labor statistics by preventing you from  
:: getting fired for accidentally running some script.     
:: ---------------------------------------------------------------  
@echo off

::  
:: Pass ANY text as a parameter to bypass the check  
::  
if not (%1) equ () goto :start_batch

:input_check

cls

echo *******************************************************  
echo *** WARNING THIS BATCH WILL PROBABLY GET YOU FIRED! ***  
echo *******************************************************  
set /p strchoice=Are you sure you want to execute: (y/n)?  
if (%strchoice%) equ (y) goto :start_batch  
if (%strchoice%) equ (n) exit

goto :input_check

:start_batch

If you put this code into a separate batch script called dont_get_fired.bat then you could call it from your batch scripts to pause / interrupt the flow and give the user a chance to exit if they accidentally ran the wrong script.

:: ---------------------------------------------------------------  
:: Prevent accidental exeuction  
:: ---------------------------------------------------------------  
call %~dp0\dont_get_fired.bat %1

:: do something here that will probably get you fired.

Note: In the example above where I call dont_get_fired.bat %1 I have included a parameter to the script. This is how I typically allow task schedulers to run a script that has been "protected" from accidental execution. When the user double-clicks on the batch script it will run with no value for %1 and therefore ask them if they are sure they want to run the script. When a task scheduler executes the job it simply needs to pass some text to bypass the check. I typically use something like: run_some_job.bat no_pause.

I hope you have found this overview to be useful and that you are able to see how batch scripts can not suck (or at least suck less). Please pass this along and help others join us in the fight against batch scripts that suck.

Cheers!