It’s time for another installment of Nilpo’s Scripting Secrets. In the third edition of my series featuring insider scripting tips, you’ll learn how to write binary files in two different ways, a useful trick when working with arrays, and how to bring your scripts to life with sounds and spoken text. Today’s article is mostly about having fun!
I’ve been using the ADODB.Stream object to create binary files in WSH for quite some time now. I discovered this method a while back while trying to find a way to create Compressed Folders natively.
I began by opening a newly-created Compressed Folder in a standard hex editor. I could see that the file was a simple run of twenty-two bytes as follows.
If I could find a way to write those twenty-two bytes to a file, I would be able to create an empty compressed folder by creating its binary contents directly. This proved slightly difficult, because neither VBScript nor WSH provide a means of writing binary information to a file.
strPath = "C:Zip.zip"
Const adTypeBinary = 1
Const adTypeText = 2
Const adWriteChar = 0
Const adSaveCreateNotExist = 1
Const adSaveCreateOverwrite = 2
With CreateObject("ADODB.Stream")
I knew from previous programming experience that I would need an IStream object to write binary data. That's the programming object required to manipulate binary streams. A little research showed that the ADODB.Stream object returned an IStream object, so I began playing around to see if I could get it to do what I wanted.
The ADODB stream provided two methods for writing files from the stream: one for binary and one for text. Unfortunately, the binary method required a byte array as input. VBScript, not being designed for binary use, does not provide any means of creating a true byte array, so I had to develop a workaround.
I was able to write text to the stream character by character. I used VBScript’s little-known ChrB function to write binary characters to the stream one at a time. Saving the file and closing the stream wrote the binary data to the file as I had hoped.
.Open
.Type = adTypeBinary
.LoadFromFile strPath
.Position = 2
arrBytes = .Read
.Position = 0
.SetEOS
.Write arrBytes
.SaveToFile strPath, adSaveCreateOverwrite
.Close
End With
I thought that I had it figured out, until I tried to open the new compressed folder. Windows politely informed me that the file was corrupt. Examining the new file with the hex editor revealed the problem. The WriteText method had added two unwanted bytes to the beginning of the file before it wrote my binary information. There wasn’t any way to avoid this happening, so I needed a way to remove the unwanted bytes after creating the file.
With the file created, I could change the ADODB Stream type to binary and read in the binary information from my file. I could now manipulate the stream contents and write it back to the file as binary—something I couldn’t do before.
An ADODB stream is similar to a record set in that you can read it a little bit at a time, and a cursor marks your position in the stream. By manually setting the position to 2 and then reading the stream contents, I could read in the binary information I wanted while skipping the first two unwanted bytes.
Then, I returned the cursor position to the beginning of the stream and used the SetEOS method to set a new End Of Stream marker. This effectively clears the contents of the stream before writing back the cleaned-up binary information.
I used the ADODB.Stream method of creating binary files for quite a while before stumbling onto this next solution. The idea was originally published by Eric Phelps, but unfortunately I don’t have a link to that.
While the ADODB method is effective, it does have one major drawback. Each file has to be written to disk twice. While this is a fairly quick process, just knowing that it was happening made the method seem inefficient to me. This method doesn’t post that problem.
strPath = "C:Zip.zip"
Const ForWriting = 2
Set objFso = CreateObject("Scripting.FileSystemObject")
This method exploits the FileSystemObject’s ability to create an empty file on disk. The OpenTextFile method has an optional third parameter that, when set to true, will create a new file on disk if the specified file doesn’t already exist. This file is created without any contents. It’s just an empty shell. Writing binary data directly to that file would accomplish the task at hand.
The method uses an interesting manner to write the binary data. A string of byte characters is read in a For loop, two characters at a time. A “&h” is then added to the beginning of the two-character string. You may recognize this as VBS’s way of explicitly writing hexadecimal values.
The string containing the hex value is then converted to a decimal number with the Clng function. Once you have a decimal representation of the binary character, it can be written directly to the file with the Chr function.
Admittedly, VBScript does not provide very good support for arrays when compared to some other scripting languages. It provides the basic array functions but seems to lack any real method of performing advanced manipulation on an array. Therefore, when the need arises, we’re stuck with having to write our own functions. This function is one I wrote in response to the question “how can I remove elements from an array?”
arrTest = Array()
ReDim arrTest(2)
arrTest(0) = "Item 1"
arrTest(1) = "Item 2"
arrTest(2) = "Item 3"
Delete arrTest, 1
Let’s assume we have an array that looks like the example above. VBS doesn’t provide any way to remove an element from that array. To do that, I’ve written a Delete routine that accepts two parameters. The first is the array to work with and the second is the index of the element to remove.
Sub Delete(ByRef arrArray, intIndex)
For i = intIndex To UBound(arrArray) - 1
arrArray(i) = arrArray(i + 1)
Next
ReDim Preserve arrArray(UBound(arrArray) - 1)
End Sub
The routine itself is not complicated; it’s going to loop through each item in an array and rewrite it to a new position.
If you think about it, when you remove an array item, each item after that element will be moved to the next index down, replacing the element that was removed. The array size is then shortened to remove the duplicate item that ends up at the end. That’s exactly what this function will do.
By starting the For loop at the index position of the element to be removed, we can overwrite that element with the contents of the one that follows it. The loop then steps through each remaining element in the array, doing the same thing. Finally, the ReDim statement is used to remove one element from the array, leaving only those that we wanted.
It’s nice when your script can pass information back to the user, but it’s sometimes inconvenient to display a message box for simple notifications. And what if you want to add some accessibility to your scripts for the sight-impaired? Why not teach your scripts how to speak?
That’s right! Adding voice to your scripts can be both useful and fun. Not only can you teach your scripts to speak, but you can also teach them to read through the use of Microsoft’s Speech API (SAPI).
strText = "Hi, I am a talking script."
Set objVoice = CreateObject("SAPI.SpVoice")
objVoice.Speak strText
As you can see, making a script talk is extremely easy. You simply connect to the SAPI Voice object and use the Speak method. The text will immediately be read back by the Text-To-Speech Engine.
Const ForReading = 1
Set objVoice = CreateObject("SAPI.SpVoice")
Set objFso = CreateObject("Scripting.FileSystemObject")
Set objFile = objFso.OpenTextFile("C:speechraven.txt", ForReading)
Do Until objFile.AtEndOfStream
objVoice.Speak objFile.ReadLine
Loop
With slightly more effort, your script can read the contents of a text file. Here I’m using the FileSystemObject to open and read a simple text file line by line while passing that text to the Speech API.
You can find the files used in this example here: speech.zip
Set objVoice = CreateObject("SAPI.SpVoice")
Set objStream = CreateObject("SAPI.SpFileStream")
objStream.Open "C:speechraven.wav"
objVoice.SpeakStream objStream
Not only can you render text as speech, but the Speech API can also play back audio wave files. SAPI provides the SpFileStream object for opening and streaming the audio file. This time the SpeakStream method is used to play back the stream.
Admittedly, playing wav files in your scripts doesn’t seem all that useful to a system admin. After all, it’s probably not necessary for your scripts to serenade the end user. However, considering the fact that most system sounds are in wave format, this takes on a more practical use.
Set objVoice = CreateObject("SAPI.SpVoice")
Set objFile = CreateObject("SAPI.SpFileStream.1")
objFile.Open "C:WindowsMediaTada.wav"
objVoice.Speakstream objFile
And that, ladies and gentlemen wraps up the third issue of Nilpo’s scripting secrets. Go out and have a little fun with these techniques. Try writing a login script that will welcome the user by name! Until next time, keep coding!