Using VBScript 5.0 and regular expressions to write an alternative to getmac.exe

I had a college professor who enjoyed rewriting system commands in whatever programming language he was teaching at the time. I distinctly remember his lesson about recursion and the corresponding assignment, which required us to rewrite ls (the UNIX equivalent of dir). Although the effort seemed futile at the time, I've grown to appreciate the old man's approach, so I decided to give his exercise a try. I developed the following assignment for myself: Rewrite the Microsoft Windows NT Server 4.0 Resource Kit's Getmac utility in Visual Basic Script (VBScript) using regular expressions. In case you haven't heard of the utility, getmac.exe displays the media access control (MAC) address for each of a machine's NICs.

If you're familiar with Windows Scripting Host (WSH), you're probably wondering how I can possibly complete this assignment because WSH doesn't support regular expressions. Well, I used VBScript 5.0 (the beta 2 version is available at press time) to rewrite Getmac; VBScript 5.0 includes support for regular expressions.

To run my VBScript recreation of Getmac, which I call getmac.vbs, you need to install the latest release of VBScript 5.0. You can download VBScript 5.0 as part of Microsoft Scripting Technologies 5.0 from http://msdn.microsoft.com/scripting. The file you need is scr50en.exe. After downloading the file, run the self-extracting executable file to install the new VBScript and JScript engines. Then, you can run getmac.vbs by typing

C:\> cscript getmac.vbs

Capture MAC Addresses
Listing 1, page 184, contains the source code for getmac.vbs. The script begins by setting the Option Explicit and On Error Resume Next directives. Option Explicit requires the script to declare all its variables. On Error Resume Next initializes Err, the VBScript error-handling object.

Next, getmac.vbs declares its variables and the OpenFileForReading constant, which VBScript's FileSystemObject object will use. Then, getmac.vbs uses an If block to test for user input at the command line. The If block uses the WScript Arguments collection's Count property to determine whether the collection contains any elements. If the Arguments collection isn't empty, getmac.vbs uses VBScript's InStr function to determine whether the first argument contains a question mark (?). If InStr finds a question mark anywhere in the first argument, getmac.vbs calls the Usage subroutine, which echoes usage instructions to the user and exits the script.

At callout A in Listing 1, getmac.vbs runs NT's Ipconfig utility. When you run Ipconfig with the /all switch, the utility provides information about a machine's NICs, including each NIC's MAC address. Getmac.vbs must create a WScript Shell object before it can run the ipconfig.exe command. The script uses WScript's CreateObject method and passes CreateObject the WScript.Shell ProgId to tell the function to create a WScript Shell object.

After creating objShell, getmac.vbs uses the Shell object's Run method to execute ipconfig.exe. If WSH supported standard I/O (STDIO), I could have redirected the output of Ipconfig directly into the script. Because WSH doesn't support STDIO, I had to save the command's output to the machine's hard disk and retrieve it from the disk before I could use it within the script. Getmac.vbs uses the command processor's standard output redirection symbol (>) to save Ipconfig's output to the file C:\temp\ipconfig.txt. In addition, getmac.vbs uses cmd.exe's /C option to ensure that the script doesn't leave an instance of cmd.exe running after ipconfig.exe exits; /C tells cmd.exe to terminate after it runs the command. The Run method's 0 parameter tells Run to execute the command without opening a window, and the TRUE parameter tells Run to wait for the command to exit before returning to the script.

After the Ipconfig command completes, getmac.vbs checks the status of the Err object to see whether Run encountered a problem. Any value other than zero in Err.Number tells getmac.vbs that a Run error has occurred. You can obtain additional error information by examining the Err object's Description property. However, getmac.vbs doesn't look into the error's cause; the script echoes a message to the user and aborts the script if Err has a value other than zero.

Because I had to write the Ipconfig output to disk, getmac.vbs's next step must bring the Ipconfig data into the script. At callout B, getmac.vbs uses VBScript's FileSystemObject object to accomplish this task. You must create an instance of FileSystemObject before you can use it. Therefore, getmac.vbs uses VBScript's CreateObject method. Getmac.vbs passes CreateObject the ProgId for FileSystemObject.

After getmac.vbs creates the objFileSystem object, the script invokes the object's OpenTextFile method and passes OpenTextFile the name of the file getmac.vbs stored Ipconfig's output in and the name of the constant the script initialized. The OpenFileForReading constant tells OpenTextFile to open the file as read-only. OpenTextFile returns a reference to a TextStream object, which initializes objTextStream.

When getmac.vbs has established the objTextStream reference, the script uses the TextStream object's ReadAll method to slurp the entire file into the strIpFileText string. Then, getmac.vbs checks the Err object for error conditions. If getmac.vbs finds no error, the script uses the Close method to close the ipconfig.txt file and continues.

Use a Regular Expression to Retrieve Data
Getmac.vbs's fun begins at callout C, where the code satisfies my assignment's regular expression requirement. Regular expressions provide a powerful method for searching text. Regular expressions are a set of mnemonics that you can use to define a pattern describing the text you're searching for. You can add a regular expression pattern to a script and have the script compare the pattern to the target text. When it finds a match, the script returns the matching text. The best way to grasp regular expressions is by example, so let's return to getmac.vbs.

The first thing you'll notice at callout C is the VBScript new operator. As you might suspect, new creates objects. Getmac.vbs uses new to create a RegExp object and initialize the objRegex reference to the RegExp object that new returns. (VBScript 5.0 introduces the new operator and support for classes, which I'll cover in a future column.)

After initializing objRegex, getmac.vbs is ready to use the RegExp object. First, the script must set the RegExp object's properties. The RegExp object supports three properties: Pattern, Global, and IgnoreCase. Pattern tells a RegExp object what to search for. Global tells the object whether to find all occurrences of the pattern in the target string or just to find the first occurrence in the string. IgnoreCase's function is self-explanatory.

You can see in callout C that the first regular expression pattern that getmac.vbs searches for is

\[0-9a-f\]+-\[0-9a-f\]+-\[0-9a-f\]+-\[0-9a-f\]+-\[0-9a-f\]+-\[0-9a-f\]+

Getmac.vbs uses this pattern to find the MAC addresses that the strIpFileText string contains.

What does this pattern mean? To understand what getmac.vbs is looking for, you need to look at the text the script is applying the pattern to. Screen 1 shows the standard output of the ipconfig.exe utility. By the time getmac.vbs reaches the code at callout C, the strIpFileText variable contains the output from the Ipconfig /all command as a continuous string. To duplicate getmac.exe, getmac.vbs needs to extract two types of information from the strIpFileText string: MAC addresses and corresponding adapter names.

A MAC address consists of six groups of hexadecimal digits separated by hyphens. Getmac.vbs's first RegExp Pattern property tells the script to search for just that type of text. Let's break the pattern into digestible pieces. You can see that the pattern is a recurring sequence of

\[0-9a-f\]+-

The \[0-9a-f\] part of the pattern tells the RegExp object to find any numeric character between 0 and 9 or alphabetic character between a and f. The plus symbol (+) tells the RegExp object to match the preceding character range one or more times. The hyphen (-) following the plus symbol tells the RegExp object that each of the groups of one or more characters from 0 to 9 or a to f must precede a hyphen. Notice that the last element in the pattern does not include a hyphen because no hyphen follows the last digit of a MAC address. You can describe getmac.vbs's first RegExp Pattern property in plain English by saying, "Find one or more characters in the range of 0 to 9 or a to f that is immediately followed by a hyphen, which in turn is followed by one or more characters in the range of 0 to 9 or a to f followed by a hyphen, and so on for six groups of characters (the last group is not followed by a hyphen)."

After getmac.vbs sets the Pattern, Global, and IgnoreCase properties, the script invokes the Execute method and passes Execute the strIpFileText string to which the script needs to apply the regular expression that describes MAC addresses. Execute searches strIpFileText for text that matches the pattern and returns a Matches collection that contains the match results. The script stores the Matches collection in colRegexMatches1.

Next, getmac.vbs changes the Pattern property to

\w+:

The \w tells the RegExp object to look for words, so this pattern means that the object needs to return occurrences of one or more words that immediately precede a colon (:). The script invokes the Execute method a second time and stores the results in the colRegexMatches2 collection. (Execute uses the first search's Global and IgnoreCase properties because getmac.vbs doesn't change those properties.)

Then, getmac.vbs echoes the results to the console. The script echoes header information first, then loops through the colRegexMatches1 collection. Through each iteration of the For Each...Next loop, getmac.vbs assigns to the variable element an item from the colRegexMatches1 collection. Getmac.vbs accesses the corresponding item in the colRegexMatches2 collection (i.e., the name of the NIC that corresponds to the element MAC address) in the body of the For Each...Next loop. The index variable i and the Matches collection's Item property make this functionality possible.

A colon follows each adapter name in colRegexMatches2 because of the regular expression pattern that getmac.vbs uses to find NIC names. Before getmac.vbs displays each NIC name in the For Each...Next loop, the script uses VBScript's Split function to remove the colon from the end of the NIC's name.

After displaying the results, the script deletes the file that the Ipconfig command creates, releases all the objects that the script creates, and exits. Screen 2, page 185, shows the format in which getmac.vbs displays results.

Moving On
Unfortunately, the VBScript 5.0 documentation (which you can find at http://msdn.microsoft.com/scripting) doesn't contain much information about regular expressions. The JScript 5.0 documentation provides more information about the RegExp object and pattern syntax than the VBScript documentation provides, but the JScript information is weak and expects readers to have a background in regular expressions.

Because Microsoft doesn't provide comprehensive documentation about regular expressions and because the RegExp object's pattern syntax closely resembles Perl, I encourage you to pick up one of the many Perl books that cover regular expressions. In addition, you can find volumes of regular expression information on the World Wide Web. Simply point your browser to your favorite Internet search engine, and search for the phrase regular expressions.

So how did I do on my assignment? Well, I wrote a script that provides basically the same functionality as getmac.exe, and I met the regular expression requirement. My script is a good solution unless you need to access MAC addresses on remote machines. The resource kit's getmac.exe supports remote machines, so I'll make adding remote functionality to getmac.vbs your homework assignment. Want a hint? Use the Win32_NetworkAdapter class in Windows Management Instrumentation (WMI—for more information, see "Systems Management Panacea," April 1999). Have fun!