My five VBScript 'best practices'

I've been programming in VBScript for a while now. Over that time I've seen some things that for better or worse made me take note. I thought that I would share those, in an attempt to at worst open a dialogue on some level and at best convince some VBScripters to slightly modify their actions.


1. Misuse of On Error Resume Next


The On Error Resume Next statement can be quite useful, but all too often it finds its way to the top of a VBScript. Doing this will basically render the entire script free of errors. The statement does have its uses though. Lets say that I want to open a file for writing. Lets consider the things that could go wrong

  • No Permission to write there
  • File exists already
  • File is locked
  • Disk is full
  • User doesn't have enough disk quota space
  • Saving to Network Drive that is offline
We could spend half a day and code around each one of these items. Instead we could do the following:

On Error Resume Next

If Err Then
HandleError
Err.Clear
End If
On Error Goto 0

What happens here is that everything between the On Error Resume Next and the On Error Goto 0 statement don't raise errors. Everything before and after will still cause issues. We can test for errors by checking the Err variable. It returns true for non-zero numbers. So if we have an error code we enter the condition and handle the error. Once done handling our error we call Err.Clear. This clears out the error code so when we test it later in the code we don't get this error. The last statement simply turns error suppression back off.

Also, if you call On Error Resume Next in a function or subroutine then it will only stay in effect for that function and all calls made by that function.

I prefer the second option, just because I like knowing where error suppression starts and stops. With the second option, you can turn on error suppression in one function, call a second function and still have error suppression turned on.

My main point in all of this, is that you have to know where your script is failing. Covering up the problems will curse you sooner or later. Its ok to use error suppression, but so long as you are handling the problems when they arise.

2. Not using Option Explicit

I'd almost like to say as a general rule, you should take both this and the previous rule together and for every script that has On Error Resume Next as the first line of code, should be replaced with Option Explicit. In fact most every script should have Option Explicit as the first line.

What Option Explicit will do for you, is make sure your variables are declared. It does this, by not allowing undeclared variables in your code. So if I said

Option Explicit

i = 0

I'd get an error of "Variable is undefined 'i'"

I'd have to add

Option Explicit

dim i
i = 0

You can add multiple variables by separating them by commas

dim LastName, FirstName, BirthDate

All this will save you the hassle of debugging your code only to find out that you mistyped your variable name.

This may seem like a lot of work to do. Ultimately though, it is only really 2 lines of code. Once to turn this option on and a second to declare your variables. What you gain is completely eliminating one of the more common reasons why scripts fail.

3. Don't repeat yourself

I've seen a lot of scripts that are three or even four times longer than they need to be. This happens because they are simply copying and pasting their code and only modifying one or two parts. Whenever you find yourself in this situation, you should immediately start thinking of sub routines. Sometimes you may not notice until after you've written your script. In those cases, if you ever think that you'll need to modify this script you should go back and refactor it correctly.

Here is an example of what we can refactor.

ItemToWrite = Items(ItemOne)
WshShell.RegWrite "HKLM\Software\MyProduct\ItemOne", ItemToWrite

ItemToWrite = Items(ItemTwo)
WshShell.RegWrite "HKLM\Software\MyProduct\ItemTwo", ItemToWrite

ItemToWrite = Items(ItemThree)
WshShell.RegWrite "HKLM\Software\MyProduct\ItemThree", ItemToWrite

This gets refactored into

WriteKey "HKLM\Software\MyProduct\ItemOne",Items(ItemOne)
WriteKey "HKLM\Software\MyProduct\ItemTwo",Items(ItemTwo)
WriteKey "HKLM\Software\MyProduct\ItemThree",Items(ItemThree)

Sub WriteKey(KeyPath,ItemToWrite)
WshShell.Regwrite KeyPath,ItemToWrite
End Sub

Now we have one line per item instead of two and a single point where we are writing to the registry.

But, lets go a bit further. In this code we repeat "HKLM\Software\MyProduct\" three times. What happens if we misspelled one of those lines. Worse is if we need to change it. We now have to change three lines. Lets change this to

Const MYKEY = "HKLM\Software\MyProduct\"
WriteKey MYKEY & "ItemOne",Items(ItemOne)
WriteKey MYKEY & "ItemTwo",Items(ItemTwo)
WriteKey MYKEY & "ItemThree",Items(ItemThree)

Sub WriteKey(KeyPath,ItemToWrite)
WshShell.Regwrite KeyPath,ItemToWrite
End Sub

We did add a line, but in turn we saved our self the trouble of changing three lines whenever we change that key path.

Whenever you intend to use a string or a number several times, it is best to simply put it in a constant and simply reference it. If you need to modify the string once and then use it multiple times, I'd recommend just using a variable instead of a constant. Either way it will help you when modifying your code.

4. Not using descriptive variable names

In some of my examples, you may see me using a single letter for a variable (Such as i). In practice though this is only good in a few specific examples. Many scripts that are giving examples of some technique will often use a non-descriptive variable(s). I think this leads to individuals using non-descriptive variables in their actual code. Your script will determine your level of descriptiveness, but sometimes just saying 'Name' can be good. Othertimes you'll need "LastName'. Either is better than 'n'.

I deviate from this in one distinct way. When using a loop that has a variable to be used as the index of an array I'll likely use just 'i'. This is fairly common. Sometimes nested for loops for will i and then j. At this point you may be better off using 'Row' and 'Col'. Again, that all depends on your situation. In those situations though, you are using a variable in a single location and not interspersed throughout your code.

5. Not using Comments

I look at programming/scripting languages as a compromise in communicating your actions with the computer and with other users. Comments allow us to extend our communication with the user. By user, I don't just mean you. I also mean the next person who gets the 'joy' of reading your code.

A lot of people do believe in writing comments, but still don't get it right. A comment should be used to say why you did something, not what you did. Most mediocre scripters will be able to take your script and decipher what you did. What they won't be able to do, is figure out why you wrote the code you did.

Taking this approach can sometimes mean writing less comments than you normally would. Jeff Atwood expanded on the idea of commenting on why you did something and had some excellent suggestions on what you can do to be more descriptive with your intent.

Using Devcon

A while back I wrote about using SetupAPI functions in VB.NET. The goal was to be able to work with devices and ultimately enable and disable a device.

The code was what I would eventually use, but initially I used Devcon. Devcon is essentially the Device Manager over a command line. It is freely available. You can even get the source code from the Windows Server 2003 Driver Development Kit (DDK). It may be available in the newer Windows Driver Kit (WDK) as well.

To demonstrate the usefulness of devcon, we can look at some examples from my blog and see how they coorelate to devcon commands

Retrieving Device Setup Class Names and Getting Friendly Class Names:
We can use the following command

devcon classes

This would give us output similar to:

Listing 53 setup class(es).
WCEUSBS : Windows CE USB Devices
USB : Universal Serial Bus controllers
PnpPrinters : IEEE 1394 and SCSI printers
Dot4 : IEEE 1284.4 devices
Dot4Print : IEEE 1284.4 compatible printers
CDROM : DVD/CD-ROM drives
Computer : Computer
DiskDrive : Disk drives
Display : Display adapters
fdc : Floppy disk controllers
hdc : IDE ATA/ATAPI controllers
Keyboard : Keyboards
MEDIA : Sound, video and game controllers
Modem : Modems
Monitor : Monitors
Mouse : Mice and other pointing devices
MTD : PCMCIA and Flash memory devices
MultiFunction : Multifunction adapters
Net : Network adapters
NetClient : Network Client
NetService : Network Service
NetTrans : Network Protocol
PCMCIA : PCMCIA adapters
Ports : Ports (COM & LPT)
Printer : Printers
SCSIAdapter : SCSI and RAID controllers
System : System devices
Unknown : Other devices
FloppyDisk : Floppy disk drives
Processor : Processors
MultiPortSerial : Multi-port serial adapters
SmartCardReader : Smart card readers
VolumeSnapshot : Storage volume shadow copies
1394Debug : 1394 Debugger Device
1394 : IEEE 1394 Bus host controllers
Infrared : Infrared devices
Image : Imaging devices
TapeDrive : Tape drives
Volume : Storage volumes
Battery : Batteries
HIDClass : Human Interface Devices
61883 : 61883 Device Class
LegacyDriver : Non-Plug and Play Drivers
SDHost : Secure Digital host controllers
UsbPcCardReader : USB PC-Card Readers
Avc : AVC Device Class
Enum1394 : IEEE 1394 IP Network Enumerator
MediumChanger : Medium Changers
NtApm : NT Apm/Legacy Support
SBP2 : SBP2 IEEE 1394 Devices
Bluetooth : Bluetooth Radios
WPD : Windows Portable Devices
USB : Motorola USB Device

You'll note that we aren't receiving the Class Guid. I'm not sure if this is that important anyway.

Actually Getting to the Devices and Getting Device Names:

There are a couple of ways we can do this. The first option is to simply specify a class. That happens with the = argument

devcon find =net

PCI\VEN_14E4&DEV_169D&SUBSYS_105E147B&REV_11\4&1B41B794&0&00E0: Broadcom NetLink (TM) Gigabit Ethernet
ROOT\CNTX_VPCNETS2_MP\0000 : Linksys Wireless-G USB Network Adapter #9 - Virtual Machine Network Services Driver
ROOT\CNTX_VPCNETS2_MP\0009 : Broadcom NetLink (TM) Gigabit Ethernet - Virtual Machine Network Services Driver
ROOT\CNTX_VPCNETS2_MP\0010 : Linksys Wireless-G USB Network Adapter #10 - Virtual Machine Network Services Driver
ROOT\MS_L2TPMINIPORT\0000 : WAN Miniport (L2TP)
ROOT\MS_NDISWANBH\0000 : WAN Miniport (Network Monitor)
ROOT\MS_PSCHEDMP\0001 : Broadcom NetLink (TM) Gigabit Ethernet - Packet Scheduler Miniport
ROOT\MS_PSCHEDMP\0002 : Linksys Wireless-G USB Network Adapter - Packet Scheduler Miniport
ROOT\MS_PTIMINIPORT\0000 : Direct Parallel
...
31 matching device(s) found.

We can also use

devcon listclass Net

To find all devices, even those that aren't present, we type

devcon findall =Net

We can also filter based on how the device is connected. This command will show just PCI devices in the net class

devcon find =Net PCI\*

You can do this without even specifying the class as well.

devcon find PCI\*

Getting Status on a Device:

Similar to the last set of commands we can use

devcon status =Net

PCI\VEN_14E4&DEV_169D&SUBSYS_105E147B&REV_11\4&1B41B794&0&00E0
Name: Broadcom NetLink (TM) Gigabit Ethernet
Driver is running.
ROOT\CNTX_VPCNETS2_MP\0000
Name: Linksys Wireless-G USB Network Adapter #9 - Virtual Machine Network Se
rvices Driver
Driver is running.
ROOT\CNTX_VPCNETS2_MP\0009
Name: Broadcom NetLink (TM) Gigabit Ethernet - Virtual Machine Network Servi
ces Driver
Driver is running.
ROOT\MS_L2TPMINIPORT\0000
Name: WAN Miniport (L2TP)
Driver is running.
ROOT\MS_NDISWANBH\0000
Name: WAN Miniport (Network Monitor)
Driver is running.
ROOT\MS_PSCHEDMP\0001
Name: Broadcom NetLink (TM) Gigabit Ethernet - Packet Scheduler Miniport
Driver is running.
ROOT\MS_PSCHEDMP\0002
Name: Linksys Wireless-G USB Network Adapter - Packet Scheduler Miniport
Driver is running.
ROOT\MS_PSCHEDMP\0010
Name: Linksys Wireless-G USB Network Adapter #7 - Packet Scheduler Miniport
Driver is running.
ROOT\MS_PTIMINIPORT\0000
Name: Direct Parallel
Driver is running.
...
31 matching device(s) found.

The same arguments as last time can be used.

Enabling/Disabling a device:

You can use wildcards again, although I usually am very explicit with what I am enabling or disabling.


devcon disable @"HDAUDIO\FUNC_01&VEN_10EC&DEV_0880&SUBSYS_147B9B01&REV_1008\4&EEB356C&0&0001"
HDAUDIO\FUNC_01&VEN_10EC&DEV_0880&SUBSYS_147B9B01&REV_1008\4&EEB356C&0&0001: Disabled
1 device(s) disabled.

devcon enable @"HDAUDIO\FUNC_01&VEN_10EC&DEV_0880&SUBSYS_147B9B01&REV_1008\4&EEB356C&0&0001"
HDAUDIO\FUNC_01&VEN_10EC&DEV_0880&SUBSYS_147B9B01&REV_1008\4&EEB356C&0&0001: Enabled
1 device(s) enabled.



All this works quite well in most situations. There were a couple reasons why I eventually opted with the VB.NET way of doing things. First, efficiency. There is a lot of overhead to enabling and disabling a device with Devcon. This can slow down a program, especially when enabling and disabling a device can be quite time consuming in a computers way of imagining time. 500 milliseconds can be an eternity. 2-3 seconds can be a long time for a user to look at no progress.

The Second reason is just the nature of devcon. Since it is meant to be similar to the Device Manager itself, it acts like the Device Manager. This means devices are disabled on a per-profile basis. So, if you diable a device in one hardware profile and then boot in a different profile, it won't continue to be disabled. In my particular project, this wasn't ideal.

If these reasons aren't compelling enough for you to code this in .NET or C++, then you should look at using Devcon to accomplish your task.

Using RunOnceEx

Last week we figured out how to run something once per user. This week, we will look at running something once on a computer. I'm sure that there are fifty different ways to do this. Personally I like this method, because of three features:

  1. Easily able to run multiple items
  2. Easily able to sequence items
  3. Shows progress with a GUI
RunOnceEx provides us with all of that. We'll start with the registry key.

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnceEx

Inside of that is a value name you will create called TITLE (REG_SZ). This will be the title displayed on the window showing the progress of our installation.

We then create keys under that. For convenience sake most people number them. This also will help to sequence installs. Inside each key, the default value will be the string that will be displayed when installing items in the key. You create values (REG_SZ), each will be ran in alphabetical/numeric order. The data can be pretty much anything. MSI, EXE, BAT, VBS. I'm not sure what else you'd need, but I think its covered.

Once you've populated this registry structure you can reboot/logoff. The next login that has rights to modify the RunOnceEx registry key will execute the keys in order. Alternatively you can execute the RunOnceEx Process anytime with the following command:

rundll32.exe iernonce.dll,RunOnceExProcess

Please note that the DLL used here is from Internet Explorer. At one point this command did not work when running IE7. I believe this issue may have been addressed. If not, the common workaround I've seen, is the use of the original IE6 dll and simply explicitly calling its path. Regardless, simply logging in will kick off the process.

Here is a sample of the registry for RunOnceEx. Simply copy this into a file and save with a .REG extension.

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnceEx]
"TITLE"="Installing Stuff"

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnceEx\1]
@="Notepad"
"1"="C:\\WINDOWS\\notepad.exe"

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnceEx\2]
@="Calculator x 2"
"1"="C:\\WINDOWS\\System32\\calc.exe"
"2"="C:\\WINDOWS\\System32\\calc.exe"



Here is what the above item will look like











What will happen with the above example is Notepad will run, followed by two calculator processes. Each process must end, before the next process begins.

References:
MSFN

Using Active Setup

On more than one occasion I've had the need to change a user setting. These settings may be replacing a file or a registry key.

Possibly the first idea in solving this problem is to simply go through each user folder in Documents and Settings and work with each folder in sequence. By doing this, you'll likely have some permission problems and also, if working with the registry, have the overhead of loading each registry hive for each user. I've also found that depending on your environment you may be modifying profiles that are stale or completely obsolete.

What we can use instead, is Active Setup. In a nutshell, Active Setup runs when a user logs into the workstation and compares their registry with the local machine. For each entry found missing with the user's registry, it is executed. Normally an entry will be executed once per user and at the time the user logs in. You'll know it is running based on a small window in the top left corner popping up when the user logs in.

To get started You should add the following


[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Active Setup\Installed Components\UniqueID]
"Version"=""
"Stubpath"=""
@=""

The UniqueID is just that. If you look you'll see this is mostly Guids, but it doesn't have to be. Of course you'll want this to be unique.

Version is the version of what you'll be running. Typically you won't have to change this. Where it comes in handy is if you want to update this script and run it again under the same UniqueID. If you update the version then the entry will be ran again.

The Stubpath (Command) is the command that will be executed. I have seen MSIs, EXEs, and VBScripts all run without issue. I've also seen RunDll calls as well.

The default valuename (@) is what will be displayed when the command is being executed.


References:
Active Setup - WikiDesktop
etlengineering.com