Writing VBScript that really runs in PowerShell

Download Invoke-ActiveScript wrapper and demos

It’s quite possible to run traditional WSH script from the PowerShell console, and people have even directly hosted VBScript in PowerShell since 2004 using the Microsoft Script Control. However, you can go farther than just loading VBScript into an object in a PowerShell session and calling its methods. You can use VBScript – or any other Active Scripting language, for that matter – as a scripting language for PowerShell.

Although the Script Control can be a pretty useful object in some situations, not many people know about the details of how it works; and for the most part, you don’t need to know a lot to just use it. So I’m going to skip the details of why you want to do things a particular way, and simply describe how we ideally want a PowerShell-hosted script to operate, then demonstrate a PowerShell script that can be used to run Active Scripting language scripts. I’ll be talking about VBScript to simplify discussion, but we can really use any Active Scripting language.

Structure for a PowerShell-hosted Active Script

PowerShell scripts or functions that work on piped objects have 3 blocks that roughly correspond to functions in VBScript or JScript. The first block, called begin, runs when the script is starting up. The second block, process, runs once for each object passed through the pipeline. The final block, end, runs when the last object has passed through the pipeline and things are shutting down.

Making VBScript run within PowerShell the right way means creating scripts that follow the same form. If you follow some simple implementation rules, it is then possible for a simple universal PowerShell script to run any VBScript you want to run inside PowerShell. I’ve created a generic script to do this called Invoke-ActiveScript, shown at the end of this post; here’s how you want to write a PowerShell VBScript that works correctly with it.

The VBScript file can contain global code and specific functions, classes, and anything else you normally keep in VBScript files. However, all global code runs immediately when we load the file, so that isn’t helpful for processing pipeline input. However, it does allow us to define constants or variables that will be used within functions in the script.

The script should contain one or more of the following three specially-named functions. ScriptBegin will run when pipeline processing starts up; it will have no arguments. This is where you can initialize values, although you can also do so globally. ScriptProcess will run once when each object passes through the pipeline. It MUST have a single argument, a variable that refers to the current pipeline object that PowerShell will pass into the script. You can use any name for this object, but I use PSObject for convenience. Finally, ScriptEnd will run when the pipeline is shutting down. This is where you want to shut down things such as COM objects. Each of these functions can return data (and will do so automatically) but be aware that anything coming out of the functions will become output in the pipeline.

So the generic form for PowerShell VBScripts is something like this:

Function ScriptBegin()

End Function
Function ScriptProcess(PSObject)

End Function
Function ScriptEnd()

End Function

Example Scripts

This is VBScript that assumes it gets text (or something it knows how to turn into text) from PowerShell, then uses the Escape function to turn it into an escaped string. It doesn’t need initialization or shutdown; it just escapes whatever it gets from PowerShell and returns the escaped value, which gets passed into the PowerShell pipeline.

'EscapeString script
Function ScriptProcess(PSObject)
    ScriptProcess = Escape(PSObject)
End Function

It doesn’t get much simpler than that. Now, how do you actually run this? Assuming you’ve saved the EscapeString code to the file C:\tmp\EscapeString.txt (note that the file extension doesn’t matter) and that Invoke-ActiveScript is in your search path, you would run it like this:

Invoke-ActiveScript c:\tmp\escapestring.txt

the same thing applies to using JScript. Invoke-ActiveScript assumes the language you’re using for the script is VBScript by default, but you can explicitly specify another language engine. Suppose you use the following one-liner JScript code, saved to the file c:\tmp\escapestring2.txt:

function ScriptProcess(PSObject){return escape(PSObject)}

You can run the code like this:

Invoke-ActiveScript c:\tmp\escapestring.txt jscript

These are pretty trivial examples, of course. It can end up being very useful if you need to invoke methods of COM objects that can’t be automatically wrapped by .NET (and therefore PowerShell). I’ll be posting further examples of using the technique later.

Invoke-ActiveScript.ps1

The PowerShell script that wraps up Active Script hosting is shown below; you should be able to simply copy-paste it and have working code.

This lacks the bells and whistles I would like to see in an advanced host, but it’s worked fine for me in this form since late 2004. There are some limitations to it that I’d like to address in the future.

Microsoft has never gotten around to implementing a 64-bit version of the Script Control. This is a significant problem moving forward, since on 64-bit Windows, 64-bit processes (such as the default PowerShell version) cannot load 32-bit controls. However, this can be addressed in PowerShell 2 with an extended script that uses Start-Job to invoke a 32-bit PowerShell process. In the interim, it’s possible to just use this from 32-bit PowerShell. The one long-term problem that could arise is that some objects can’t be easily passed between 32-bit and 64-bit code – although simple data types should be fine, and some other objects may be helped by serialization.

I dislike the need for specifying an entire file path as an argument; it would be nice to make PowerShell automatically recognize scripts like these. There’s an alternate technique for writing these scripts that I’ll demonstrate at some point. It basically comes down to using boilerplate PowerShell that wraps up ActiveScript using PowerShell’s multiline here-strings. The file can be saved with a .ps1 extension and PowerShell will automatically find and run it.

Finally, interoperability could be enhanced by adding PowerShell internal objects to the ActiveScript code object; this can be done using the Script Control’s AddObject method.

Download Invoke-ActiveScript wrapper and demos