---
title: "Batch Scripts Can Not Suck"
slug: "batch-scripts-can-not-suck"
date: 2015-08-02T05:37:55Z
author: "Justin Biard"
tags:
  - "ms-dos"
description: "In this post I look at a few ways to improve your batch scripts so they are easier to maintain and hopefully suck less."
draft: false
archive: true
---

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](https://en.wikipedia.org/wiki/History_of_Microsoft_Windows) 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](http://superuser.com/questions/319056/does-windows-still-rely-on-ms-dos) but you may still prefer the idea of DOS. You may also sometimes feel lonely... but fear not you are certainly [not alone](http://www.storagecraft.com/blog/dos-quite-dead/).

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

```batch
::----------------------------------------------------------------
:: 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](https://www.youtube.com/watch?v=svzPm8lT36o)

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

```batch
:: ---------------------------------------------------------------
:: 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:

```batch
:: ---------------------------------------------------------------
:: 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](https://icodealot.com/img/0f016d96/dos_batch_functions.png)

Here is the code I wrote for this example:

```batch
:: ---------------------------------------------------------------
:: 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:

```batch
:: ---------------------------------------------------------------
:: !!!! 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.

```batch
:: ---------------------------------------------------------------
:: 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](https://icodealot.com/img/0f016d96/dos_batch_confirm.png)

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:

```batch
:: ---------------------------------------------------------------
:: 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.

```batch
:: ---------------------------------------------------------------
:: 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!
