Over the past few months, I've started seeing a less frequently used scripting object, Scripting::Dictionary, in scripts published in articles, books, and online. However, Windows Scripting Solutions readers who I've talked to say they rarely use the Scripting::Dictionary object in their scripts. Consequently, I thought I'd show you how the Dictionary object can make things a lot easier in certain types of scripts. "Understanding VBScript: The Dictionary Object—An Alternative to Arrays," June 2000, http://www.winscriptingsolutions.com, InstantDocID 8797, covers the basics, but I go into more depth.
Objects such as WScript, WshNetwork, and Scripting::Signer are Windows Script Host (WSH) objects; the Scripting::Dictionary object is one of two key objects that are part of the Microsoft Scripting Runtime Library (scrrun.dll). The other scrrun.dll object is the well-known Scripting::FileSystemObject. Given how frequently used this latter object is, it's strange that the former is so little known. Dictionary's obscurity is probably because of its name, which can be confusing to administrators without a development background.
A Dictionary object stores data items in an arraylike structure. However, in a Dictionary, the indexes to the data items don't have to be integers. A one-dimensional array in VBScript has the indexes 0, 1, 2, ... x, where x is the upper boundary of the array that UBound(array) dictates. A Dictionary object can have the indexes (known in Dictionary terminology as keys) a, b, and c or kitchen34, sink87, and hamster999.
The primary purpose of a Dictionary object is to create a collection of related information that you can then search or otherwise manipulate. A Dictionary has a hash (known in Perl as an associative array). Each data item in a Dictionary is associated with a key, and these two related items are known as key-value pairs. Many good reasons exist to use a Dictionary object instead of a one-dimensional array, but the main ones for me are that you don't need to call VBScript's ReDim function every time you want to add an item, the Dictionary object has many useful methods and properties that arrays don't have, you can remove items without leaving gaps in the middle of your data set, and you can use strings as keys.
Dictionary Methods and Properties
The Dictionary object has six methods and four properties, which you can read about at http://msdn.microsoft.com/library/default.asp?url=/library/en-us/script56/html/jsobjdictionary.asp?frame=true. To create a Dictionary object, you use the lines
Dim dicTest Set dicTest = CreateObject _ ("Scripting.Dictionary")
with the variable name of your choice. I often use the prefix dic in the variable name, but that's optional.
Now that you have a Dictionary object, you might want to add items to it. To do so, you use the Dictionary::Add method, which takes two arguments: the key and the value to which the key refers (i.e., the item). Here are some examples:
dicTest.Add "1", _ "server43.europe.mycorp.com" dicTest.Add "2", _ "server82.oceania.mycorp.com" dicTest.Add "servername", _ "server43.europe.mycorp.com" dicTest.Add "serverIP", _ "10.123.123.123"
You could use all four lines in one script if you wanted to. In these examples, the keys are the first arguments (i.e., "1", "2", "servername", and "serverIP"), and the items are the second arguments.
If you try to use the Dictionary::Add method with an existing key, you'll get an error. However, you can use the Dictionary::Item property with an existing key to overwrite the key's old value with a new value, as in
dicTest.Item("servername") = _ "server77.europe.mycorp.com"
To see whether a particular key is in a Dictionary, you can use the Dictionary::Exists method with the key as the argument, as in
If dicTest.Exists("servername") _ Then ' Key exists. Else ' Key doesn't exist. End If
As you can see, the Exists method performs a simple Boolean test.
To remove a key and its item, you use the Dictionary::Remove method, which takes the key's name as an argument:
If the key doesn't exist, an error is generated. If you want to remove all the key-value pairs from a Dictionary and return it to an empty state, use the Dictionary::RemoveAll method:
Reading Dictionary Data
You can read items from a Dictionary in several ways. If you want to access a specific Dictionary item, you would use the aforementioned Item property to read the value. (You can use the Item property to read an item as well as to write one.) Assuming the value is printable, you could use the statement
WScript.Echo _ dicTest.Item("servername")
to print it.
Note that because the Item property can both read and write, if the Dictionary doesn't find the key that you specify with the Item property, the Dictionary will create a new entry for that key with a blank value for the item. The script that Listing 1 shows causes the Dictionary to try to read from the nonexistent key "servername". The script prints three results in sequence: 0, an empty string, and 1. The result demonstrates that the Dictionary held no items before the script tried to read from a nonexistent key and one item after the attempted read.
Note also that if you use the Item property to equate a key with a value and the Dictionary doesn't find the key, the Dictionary will create a new key and item. The script that Listing 2 shows outputs a 0, adds a key and value, then outputs a 1.
Specifying a key on the Item property is a good way to access one Dictionary item. You can access all the items in a Dictionary in two ways. First, you can use the Dictionary::Items method to extract all the items to an array in one step. This approach lets you iterate through the resulting array and operate on the items without worrying about the keys. The script that Listing 3 shows adds two items to a Dictionary, then places the items in the arrDicItems array. To iterate through the array, the script uses the Dictionary::Count property to get the number of items placed in the array. An array of 12 items will have the indexes of 0 through 11, so the script must subtract 1 from the count to iterate through the array correctly.
The second option for extracting all the data from a Dictionary is to extract all the keys from the Dictionary into an array, then use the Dictionary::Item property to iterate through the keys and pull the associated values from the Dictionary. The script that Listing 4 shows uses the Dictionary::Keys method to extract the keys to the arrDicKeys array. I emphasize the difference between method one and method two by having the script in Listing 4 print the key as well as the item. The script in Listing 3 doesn't tell us what the key for each item is.
The two approaches illustrate a key (pardon the pun) point about Dictionaries. You use a key to retrieve an item, but you can't easily use an item to find the associated key. So, no easy way exists to use the value "10.123.123.123" from the script in Listing 4 to find out that its key is "serverIP". A Dictionary::Key property exists, but it doesn't provide both a read and write interface like the Dictionary::Item property. Instead, it's a write-only property that lets you change the name of a specific key. You use it as follows:
dicTest.Key("servername") = _ "hostname"
If the key doesn't exist, the statement will add a new empty string item for the key to the dictionary. So, a rule of thumb is to use Dictionary::Items if you don't care about the keys and just want the items and Dictionary::Keys if you want to access both keys and items.
A third method to extract all the items from a Dictionary is to use a For Each...Next loop on the Dictionary object the same way you would on a collection to give you each key in the Dictionary in turn. The script in Listing 5, page 4, shows this approach.
To finish the examination of the Dictionary object, let's look at one more property that can be important, depending on the keys you use. When you use VBScript, your keys will be strings. In some situations, you might want to use mixed-case keys such as "SRV" and "srv"; "File", "FILE", and "file"; or even "voID" and "VOID". To avoid errors when adding these keys with their values, you need to ensure that the Dictionary is case sensitive. The good news is that the Dictionary object is case sensitive by default.
If you want to do a case-insensitive string comparison, you use the Dictionary::CompareMode read/write property. By default, this property has a value of 0 for binary comparisons, but you can set it to 1 for textual comparison. (The property can also have a value of 2 or higher, but these other values are really only used inside Microsoft Access and aren't for your scripts.) You set the CompareMode value after you create the object, as follows
Dim dicTest Set dicTest = CreateObject _ ("Scripting.Dictionary") dicTest.CompareMode = _ vbTextCompare
and before you add any items. You will get an error if you try to set CompareMode on a Dictionary that contains items. The constants are already defined for you, so you can use vbTextCompare and vbBinaryCompare rather than 1 and 0, respectively, to improve code readability.
Differences Between Files
Systems administrators often need to compare and contrast a couple of files. For example, you might have one file that contains unique usernames and unique machine names and a second file that contains unique machine names and IP addresses, and you want to find the machines in file 1 that don't have an IP address in file 2 and the machines in file 2 that don't have a corresponding username in file 1. Or you might have a case like mine, in which two files with hundreds of thousands of keys and values in lines of data were supposed to contain the same values but were unsorted and had missing data and thus were more difficult to compare. Let's take a look at how you tackle these kinds of problems.
The second problem is easier to solve, so let's look at that one first. You can't just sort the files and compare them line by line because after you run into a missing line in one of the files, every subsequent line generates an error. You could work around this problem by writing a routine that, when it encounters a missing line, skips that line and checks the next one. You could also read the files into an array and have a couple of loops check every item in array 1 to see whether it's in array 2, but the Dictionary object lets you write much simpler code.
To start, create two Dictionaries and populate them with the contents of the files, which are comma-separated value (CSV) files in this case. Then, check every item in Dictionary 1 to see whether it's in Dictionary 2, and vice versa, writing results to the standard output in both cases. The script in Listing 5 starts by defining three constants for the files—two input .csv files and one text file for the results—and the two usual FileSystemObject constants for reading and writing to files. After declaring the variables, the script creates the Scripting::FileSystemObject object and opens the three files—two for reading and one for writing. The script then creates each Dictionary in turn and uses two While...Wend loops to go through the two input files one line at a time and read the line into the strLine variable until the script is at the end of the file. The script then uses the VBScript Split function with the comma as a separator to pass the key and item to the Dictionary::Add method. After each While loop has finished, the script closes the relevant file. Now that the Dictionaries are populated, the script needs to compare them.
A For Each...Next loop goes through dicData1 first, pulling out each key into the strKey variable in turn. The Dictionary::Exists method then checks whether the key from dicData1 is also in dicData2. If the key doesn't exist in dicData2, the loop writes a line to the result file stating that the key exists only in dicData1. If the key exists in both Dictionaries, the loop uses the Dictionary::Item method with strKey as the argument to compare the items. If the items are the same, the loop takes no action. However, if they're different, the loop prints that fact.
After the first loop has checked every key in dicData1 to see whether it's also in dicData2, a second loop checks whether any keys exist in dicData2 that don't exist in dicData1. If the loop finds any such keys, it prints them to the result file. Finally, the script uses the Dictionary::Count property to print a line count of both input files and closes the result file. You can modify the output text as you see fit—even printing the items out if you want to. You can download two small sample .csv files—Input1.csv and Input2.csv—from the Code Library on the Windows Scripting Solutions Web site (http://www.winscriptingsolutions.com, InstantDoc ID 39312).
The script that Listing 6 shows solves the first problem I mentioned at the start of this section. The Users.csv file contains username and PC name pairs, and the Clients.csv file contains PC name and IP address pairs. Apart from the script using different variables (e.g., dicData1 instead of dicUsers), the script's set of For...Each loops is similar to that of the script in Listing 5. This time, however, the key of dicClients is the item of dicUsers, so the loops are modified to take account of that. The first loop retrieves each username (strKey) and each PC (strItem) in the Users Dictionary and checks to determine whether strItem exists in the Clients Dictionary as a key. If not, the loop writes that fact to the result file. The second loop walks the Clients dictionary. This time, each client (strKey) is an item in the Users dictionary. To check to determine whether this key corresponds to any items in the Users dictionary, the loop uses the Dictionary::Items method to retrieve an array of all the items. The loop then goes through all the items (strItem) in the array to determine whether strKey matches any of them. If it does, the loop sets the variable bolFound to TRUE. (I previously initialized it to FALSE.) After the loop has checked all the Clients Dictionary items against the Users Dictionary, if the bolFound variable is still FALSE, the loop writes out that the PC exists only in the Clients Dictionary. You can download sample Clients.csv and Users.csv files from the Code Library.
I leave you with a quick statistic: In my testing, I found that using Dictionaries was eight times faster than using arrays. When you're comparing tens of thousands of entries, that kind of performance improvement really makes a difference.