This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.
Extend the WSH Object Model with Custom Objects | |
Dino Esposito | |
Download the code for this article:Cutting1100.exe (85KB) Browse the code for this article at Code Center:WSH Demo |
|
ow can you import external code in your WSH scripts? I've been asked this question, in various forms, countless times. Let me say up front that this is not a hopelessly open issue. Microsoft has provided an effective, elegant answer with the new XML-based format of WSH 2.0. But in my experience, very few people seem to want to use the WSH 2.0 file format, WSF, preferring to stick with VBScript or JScript. This preference creates the need to import VBScript or JScript code, which can be a problem. Importing External Script Code Neither VBScript nor JScript defines language constructs that let you import external scripts in the same way HTML does (through the <SCRIPT src="�"> tag). For this reason, you must either upgrade to WSF files specific to WSH 2.0, or you must create a workaround. JScript has always provided the eval function that gives you the ability to evaluate code strings at runtime. This long-awaited feature was originally lacking in VBScript but has been available there since version 5.0. Now in both JScript and VBScript you can consider the source code to be a new data type that can be created and processed during execution time. This is particularly exciting in light of the VBScript 5.0 support for classes. If you cannot write classes in external files and reuse them without cutting and pasting, why write them?
This function reads in the content of a VBScript file and adds it to the script's global namespace through the ExecuteGlobal statement. Using the global namespace means that the code isn't executed as part of a subroutine, and doesn't use local variables. VBScript provides another statement to execute code called Execute. As you may have guessed, this runs in the scope of the function that is calling it. Using Execute instead of ExecuteGlobal in the previous function would have made the code visible only within the body of the function Include!
In JScript things are a bit more complex. The eval function is functionally equivalent to VBScript Execute rather than ExecuteGlobal. This requires a slightly different approach.
Now the Include function returns as a string the content of the JScript file that is being imported. This script code cannot be evaluated within the scope of Include; it needs to be evaluated where you want to include it:
You can do this to embed external functionality in your main script. For example, you can import the definition of a VBScript class and instantiate it through the New operator without worrying about its declaration.
If you don't specify a scripting language, then the WSH runtime considers JScript to be the default, just as DHTML treats script blocks in HTML pages.
If you import it through Include, you can start using the FileSystem object variable in your code as if it were a native system object. The following code is perfectly legal:
If you're writing a lot of WSH script code, you may need to frequently reuse a certain subset of objects in various projects. On the other hand, the WSH object model is extremely lean and doesn't provide all the functionality you may need. Storing all instances of all the global objects in a single importable file is fine, but it requires that you either upgrade to the WSH 2.0 file format or use the Include function. However, there are two ways to avoid this. I'll examine two approaches to extending the standard set of WSH objects in plain VBScript or JScript without using helper functions. Rewriting WSH from Scratch It goes almost without saying that if you rewrite the WSH executable from scratch you can make it support any object you want and publish any object model you like. WSH executables (wscript.exe in particular) are not terribly complex programs. In its simplest form, WSH is just a minimal program, minus a Windows UI, that knows how to process script code. Executing script code from within a compiled application is a matter of implementing a few COM interfaces if you're working with C++, or incorporating the Microsoft Script Control if you prefer Visual Basic.
The code on the left of the equal sign is the public name of the object. To the right of the equal sign is the progID of the object. Global.wsh must be in the same folder as the running script file. The code in Figure 2 extracts this information from the file and creates as many object instances as needed. Basically, the first line turns out to be:
The AddObject method of the Script control has the ability to inject in the scripting global namespace an item called FileSystem powered by an instance of the Scripting.FileSystemObject object. At this point, any script fileâ€"whether it's written in VBScript, JScript or Perlâ€"sees and can utilize FileSystem as if it were a native language element. Incidentally, this is the same technique that WSH employs to give you the root WScript object. A VBScript file like this
doesn't generate errors, and it works too!
then a VBScript file with only the following line is perfectly legal and works just fine:
Writing a tool that replaces WSH poses another problem. You must modify some shell settings to make it automatically process both .vbs and .js files. This requires you to tweak the HKCR\VBSfile and HKCR\JSfile nodes in the registry. Figure 3 shows the standard content of the node HKCR\VBSfile. Just replace the name of the executable responsible for processing the VBS file. Change wscript.exe to MyWSH.exe (or whatever you named your scripting host) and you're finished. Leave the rest of the command line intact. The %1 indicates the name of the VBScript file, whereas the %* groups all the command-line parameters you want to pass to the VBS file upon execution. Before you do this, if you want to test MyWSH, just compile it and drag and drop a VBScript or JScript file onto it in Explorer. (See Figure 4.)
Forcing WSH to Use Extra Objects Unless you have very special needs, I don't recommend writing a new tool to replace WSH. Instead, you could force WSH to accept custom objects. Of course, this is not an issue at all if you choose to use the new format or decide to include external files as I demonstrated earlier. The following line
creates an instance of the specified object and adds it to the scripting namespace. In WSH 2.0 you could simply add a reference to an object's type library without creating a brand new instance.
For more information on the WSH 2.0 XML file format, refer to Cutting Edge in the September 1999 issue of Microsoft Internet Developer or take a look at the WSH help in MSDN Online.
How do you intercept any call to WSH that is made throughout the system? Actually, WSH is made up of two executables: wscript.exe and cscript.exeâ€"the latter being the stripped-down, console version of the former. Each time WSH processes a script, either one of these two programs is involved. I'll consider wscript.exe for simplicity's sake only. Everything I say applies to cscript.exe too. To hit the target, you need to detect any event that turns out to be a call to wscript.exe. Fortunately, this is exactly what the IShellExecuteHook interface does. The IShellExecuteHook Interface IShellExecuteHook is a rather simple COM interface introduced with Internet Explorer 4.01 on all Win32®-based platforms except Windows CE. It was introduced in order to extend the behavior of the ShellExecute and ShellExecuteEx functions. Any time you invoke a shell object through either of these functions, the call is hooked by all registered shell extensions that provide this interface. A shell object can be a directory, a special folder, a namespace extension or a file. More importantly, ShellExecute and ShellExecuteEx are used extensively in Explorer. They are the recommended way to initiate new processes in Windows. Both functions end up calling CreateProcess, which is the only function that can start new processes in Win32. In addition, they accomplish a number of extra tasks and have a more flexible programming interface. They let you open and print documents, and enable more specific verbs on document classes. Unless you need to create processes exploiting the advanced features that CreateProcess makes available (debug mode, priority, environment settings, startup information, and the like), you should always use ShellExecute or the more powerful extended version, ShellExecuteEx.
To write a ShellExecute shell extension, start by writing an ATL object, and make sure it inherits from IShellExecuteHook as shown in Figure 5. You also must configure the registry to make the object behave like a ShellExecute shell extension. Add the following code to the ATL-generated RGS script:
Of course you should also make sure that the CLSID matches the CLSID of your shell extension object. Intercepting WSH Calls Consider that all the registered ShellExecute hooks get called each time something happens within Explorer. Any call to a context menu initiates a call to the hook, as does a double-click to explore a folder. The reason for the call is found in the lpVerb member of the SHELLEXECUTEINFO structure that the method receives. This verb will either be set to "open" or will be an empty string if the user wants to run a script. Any other verb, such as edit, explore, or print, causes the extension to return without action.
When you double-click on a VBScript or JScript file, or run it from the command prompt on the Start menu, the name of the executable contained in the lpFile field is the name of the script file. You should make sure that wscript.exe is really the program involved with the execution. FindExecutable is an API function that does this for you.
This code snippet obtains the name of the executable that's expected to run the file, and verifies that it is wscript.exe. The use of PathFindFileName makes the presence of a path in the name transparent. Notice that if you pass an EXE file name to FindExecutable, it just returns that file name, so this code always works fine.
You can only tell whether the file is a VBScript or a JScript file based on the file extension. Remember to make the file name all lower or uppercase since it's easier to work with case-sensitive string functions. In addition, remember that when the script file comes as a parameter, it may be enclosed in quotes. Just remove the quotation marks using the PathUnquoteSpaces function.
The strategy to follow should now be clear. You read in the content of global.wsh, prepare the language-specific string that creates instances of all the objects declared, and slipstream this code at the beginning of the script file. Since I've deliberately given global.wsh an INI format, you can use GetPrivateProfileSection to read its contents in a single shot. Then, it's a matter of manipulating the string to figure out both the names and progIDs of the objects. Here's how you can prepare the code string to inject it into the original script file:
With a global.wsh like the one seen earlier, and with a VBScript file involved, the code to inject looks like this:
You read the body of the script file and prefix it with the extra code. How do you run it now? You have two possibilities. The easy way is to persist the update to the script file and leave WSH to do its job. This approach has a serious disadvantage; it modifies the source code of the file. You could also write a temporary file and force WSH to run it instead of the original one. The problem here is less serious, but it is still a compromise. In fact, should the script need to test its file name (a rather infrequent occurrence, but it might happen) it would return the wrong name! To force WSH to run another file, just modify the lpFile or the lpParameters field of SHELLEXECUTEINFO accordingly.
The command line has the form of:
I'm using quotes to make sure the long file names aren't truncated. WaitForSingleObject synchronizes the execution of the current thread with the state of the specified kernel object. In this case, the object is the handle of the process just started. This means that the line after WaitForSingleObject executes only when the wscript.exe process has terminated. At that point, you can safely kill the modified script file, restore the original script file from the temporary copy, and finally delete the copy. If you don't set the hinstApp member of SHELLEXECUTEINFO to a value greater than 32 when you're about to return S_OK from Execute, you'll get a message from the shell. In practice, the hinstApp field is supposed to return the HINSTANCE of the started application to the application that called ShellExecute or ShellExecuteEx. This field normally holds the instance handle of the app that the function started. When you do this yourself and you don't set the field, the shell eagerly replies. Values below 32 are considered errors, whereas any greater value is fine. In Figure 6 you can see the complete source code for the Execute method. Conclusion The mechanism based on IShellExecuteHook is completely transparent to scripts. Just install a COM component and double-click on a VBScript file. That's it. While I mostly worked with object instances in this sample code, there's nothing in particular that prevents you from inserting generic chunks of script. |
|
Dino Esposito is a trainer and consultant based in Rome. He is the author of several books for Wrox Press, and he now spends most of his time teaching classes on ASP+ and ADO+. Get in touch with Dino at desposito@vb2themax.com. |
From the November 2000 issue of MSDN Magazine.