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.


MIND


Cutting Edge
cutting@microsoft.com       Download the code (26KB)
Dino Esposito

Windows Scripting Host 2.0
W
indows® Script Host (WSH) was designed to provide a systemwide environment that helps automate tasks across all 32-bit versions of the Windows operating system. WSH is a part of microcosm in Windows called Windows Script, within which a number of Microsoft® technologies are conveyed, including the Windows Script Engine (also known as the ActiveX® Scripting engine), COM Automation, VBScript, JScript®, and the Windows Script Components (WSC), formerly called scriptlets. WSH is a component of Windows 98 and Windows 2000, and is also available as a separate add-on for Windows 95 and Windows NT® 4.0. I covered WSH 1.0 in the June 1998 and December 1998 installments of this column. The June 1998 column provided a primer, while the December issue contained a set of advanced programming tools I developed for use with WSH.
      This month I'll examine WSH 2.0 beta 2, which is available for download from http://msdn.microsoft.com/scripting/default.htm?/scripting/windowshost/. With the distribution of Windows 2000 beta 3, you'll discover that the second edition has improved this scripting environment significantly.

The Growing Role of Scripting in Windows

      There are several new changes in WSH 2.0, but probably the most significant is the introduction of a new, XML-based file format to describe the scripts to execute. WSH has achieved great success and has turned out to be the missing link between the tasks a compiled Windows-based application can accomplish and what a user can do manually using a keyboard and a mouse.
      Until WSH came along, there was no powerful and versatile way in Windows to execute batch code to automate repetitive and administrative tasks such as login, file management, setup/uninstall, registry tweaks, shortcuts, creation of user accounts, and so forth. Creating a shortcut is quick and easy, but what if you have to create 100 shortcuts at a time? Script languages (such as VBScript, JScript, or Perl) are far easier to use than C++ or even Visual Basic®. In your everyday work, you probably have lots of tasks that you can't easily accomplish manually, but aren't worthy of even the smallest Visual C++®-based application. WSH fits this niche and, as a result, has earned its well-deserved success.
      The scripting API exposed by Windows Management Instrumentation (WMI), the Microsoft implementation of Web-Based Enterprise Management (WBEM), is a further demonstration that scripting—and WSH in particular—will be playing an increasingly central role in the Windows scenario. (WBEM is an industry initiative to develop a standardized technology for accessing management information, such as system memory, running processes, and currently installed applications, in an enterprise environment.)
      Since scripting is gaining a key position in the overall Windows jigsaw puzzle, it's no surprise that Microsoft has developed a significantly better second edition of WSH. As Andrew Clinick pointed out in his MSDN Online column (see http://msdn.microsoft.com/workshop/languages/clinic/scripting061499.asp), developers demanded a number of enhancements to make WSH development easier to use and more productive. It's heartening that some of the functionality that developers shared through articles, books, and newsgroups is now natively exposed by the core binaries. Commonly known limitations of WSH 1.0 included the lack of a system-provided mechanism to reuse code, the inability to use different languages at the same time while accessing type libraries, limited integration with development tools and the shell, poor debugging capabilities, and no standard I/O capabilities.
      While you could work around some of these points, it's definitely better and safer for Microsoft to provide a single solution. In the December 1998 issue of MIND, I presented a DLL that lets you manage drag and drop on VBScript and JScript files. If you are a loyal reader, chances are that you already hold in your hand the same drag and drop support that's built into WSH 2.0. Furthermore, by using WSC (see "Writing COM Objects with Scripting Languages" at http://msdn.microsoft.com/library/techart/msdn_xmlscripts.htm) you can already mix different languages and save your legacy code. With tricky and slightly inelegant JScript 1.0-compatible code, it's currently feasible to include external files, like so:


 eval(Include("myfile.js"))
 • • •
 function Include(file) {
    var fso, f;
    fso = new ActiveXObject("Scripting.FileSystemObject");
    f =  fso.OpenTextFile(file, 1);
    str = f.ReadAll();
    f.Close();
    return str;
 }
However, with WSH 2.0 this same feature is simpler to write—it can be reduced to a single line of code.

The New Windows Script File Format

      Any new version of a development environment poses the problem of backward compatibility, and WSH 2.0 is no exception. It does support plain old VBScript and JScript files written for WSH 1.0, but it prefers the new Windows Script (WS) file format. The same is true for the enhancements to the object model; version 2.0 exposes lots of new methods and properties while supporting everything in version 1.0. WSH 2.0 is scheduled to ship with Windows 2000. Coupled with its full backward compatibility, the migration to WSH should be seamless and painless.
       Figure 1 summarizes the new changes in WSH 2.0. As I mentioned earlier, the most important one is the new WS file format. The following code snippet shows a simple script:


 <job id="job1">
 <script language="VBScript">
   ' All the VBScript code you want
   MsgBox "Hello World"
 </script>
 </job>
This is just an enhanced version of a plain old VBScript file. In other words, this snippet shows how you can rewrite an existing file the WSH 2.0 way. The code snippet just adds a thin wrapper of XML, and doesn't yet exploit any of the new features of the WS schema.
      The following code presents a more integrated example that is admittedly useless in practice, but demonstrates the main WSH enhancements:

 <job>
    <object progid="Shell.Application" id="shellApplication" />
    <reference object="ADODB.Recordset" />
    <script language="JScript" src="MyFile.js" />
    <script language="JScript">
       shellApplication.FindFiles();
    </script>
    <script language="VBScript">
       MsgBox "adOpenStatic = " + CStr(adOpenStatic)
    </script>
 </job>
A WS file follows an XML schema that you'll find very similar to the one in WSC. WS files are XML files. The use of XML helps you build script files that can be considered more like objects than simple files; WS files are structured and well-organized, with a series of identifiable blocks of information.
      In the context of WSH, there are five XML tags that you should know: <package>, <job>, <object>, <reference>, and <script>. A generic XML file can accept any tag, provided the overall syntax of the document is compliant with the XML standard. The same is not true for WS files, which only support these five tags. In other words, WS files behave as if they are documents of a specific XML-based language even though there's no visible evidence of a Document Type Definition (DTD). (See my June 1999 Cutting Edge column for more information about DTDs and schemas in XML.)
      The main element of a WS file is the <job> tag, which identifies a batch job. A job is composed of any number of <script>, <object>, and <reference> tags. If the WS file contains just one job, then <job> must be the root tag of the schema.
      If you insert more jobs in the same WS file, then all the jobs must be enclosed in a <package> tag. A WS file can contain only one <package> tag. This tag is optional if there's only one job in the file. If you put <script> or <reference> tags outside any <job> tag with the goal of making them visible to all jobs in the package, you won't get a syntax error, but the code within these tags will be ignored. The general schema of a WS file is as follows:

 <package>
    <job>
       <script... />
       <object... />
       <reference... />
    </job>
 
    <job>
       <script... />
       <object... />
       <reference... />
     </job>
 </package>
      Any job can be identified by the ID attribute. Of course, this makes sense only for WS files that contain more than one job. Using the ID attribute, you can execute a specific job in a WS file by simply specifying a new command-line switch, such as

 wscript //job:jobID script.ws
where jobID is the ID of a job in the script.ws file.
      Let's look at the new tags in more detail. The <package> and <job> tags are just wrappers, and have ID attributes that let you identify them. The other tags (<reference>, <object>, and <script>) are much richer in programming features.
      The <reference> tag adds a reference to the type library of a given object. It allows you to import all the defined constants in your code. The <reference> tag was added just to solve one of the most frequent drawbacks of WSH programming: the common use of methods and properties that require specific constant values. Think, for example, of ActiveX Data Objects (ADO), where you often use fixed values through mnemonic literals without ever knowing about the actual value. Strings such as adUseClient or adOpenStatic have a special meaning, but who can tell on the fly about the real value you're assigning to a property?
      Until WSH 2.0 such constants were meaningless, and before using them you had to define them explicitly. In most cases you had to open the documentation—if not the type library—and read it. In the worst-case scenario, you had to import huge include files just to use a single constant.
      With WSH 2.0 you identify the type library either through the progID of the object or through the GUID of the type library (not the object's CLSID).

 <reference [object="progID"|guid="typelibGUID"] [version="version"] />
You can also specify which version of the type library you want to reference. The version attribute is a string in the form major.minor. The minor portion can be omitted. For example, to reference ADO you would use the following code:

 <reference object="ADODB.Recordset" />
However, references don't cross job declarations, and you have to repeat them for each job that needs them, even within the same package.
      The <object> tag serves the purpose of sharing an object reference through many script blocks in the same job. In a certain sense it's like object pooling, where a single connection is used across the job. For example, if you need to frequently use FileSystemObject from within different pieces of script code written in different languages, then you might avoid creating and releasing it every time by using <object> tags like so:

 <object id="objID" [classid="clsid:GUID"|progid="progID"]/>
      The object can be identified by either the CLSID or the progID. This is another minor enhancement, since you could not instantiate an object through its CLSID with WSH 1.0. Throughout the code of the job, you'll use objID to reference the object. The WSH 2.0 runtime takes care of creating and destroying the instance, as in the following code:

 <object id="shell" progid="Shell.Application" />
This line of code creates an instance of the Shell Automation object. You can refer to it using the shell ID from anywhere in the job. Notice that, like the HTML syntax, you have to prefix the CLSID with the word "clsid" if you're using a GUID to specify the object.
      The <script> tag can be used in two ways: it can contain explicit source code in any compatible script language or it can reference an external file. In either case, you have to explicitly set the language. The syntax of the <script> tag is a subset of the HTML syntax. You can use it this way

 <script language="language">
    'script goes here
 </script>
or like this:

 <script language="language" src="filename" />
      The second form has the advantage of letting you import external files in your scripts. A single job can contain multiple script blocks. These blocks share the same global environment and execute in order of appearance. An obvious exception to this general rule applies to script blocks that define functions and procedures, which aren't executed until another line of code calls them. When it's time to execute script code, the WS runtime extracts all the <script> blocks that don't contain functions or procedures and executes them in the order they appear. These blocks can be interspersed with any other block-defining procedures, as shown in Figure 2, where the main body of the job is given by the line in the last block. The JScript block in Figure 2 is never used.
      All of this code looks similar to the <script> HTML syntax, but keep in mind that WS files don't support the for or event attributes.

XML Compliance

      It's a good practice to declare your code as XML 1.0-compliant. Although I don't know of any cases where this is currently required, it marks your code for tools that will support XML 1.0 in the future. You can ensure compliance by inserting the following line at the very top of the file.


 <?xml version="1.0" ?>
      Once you've declared your code XML 1.0-compliant, you might want to wrap your script source code in a CDATA (character data) delimiter to inform the parser to consider the following text as is—in other words, without attempting to parse it. This saves you from painful headaches in case your code uses characters with special meanings, such as < or &. Figure 3 shows a slightly modified version of the code in Figure 2. The code in Figure 3 produces an error, even on a commented line (see Figure 4)!
Figure 4: Script Execution Error
      Figure 4: Script Execution Error

The point is that the XML parser attempts to analyze any content in what it knows to be a pure XML file. The & character is the standard character used to prefix XML entities—that is, XML macros usually defined by the DTD that are to be expanded by the parser. This is the reason for the odd message shown in Figure 4, which is a direct consequence of the XML 1.0 compliance assertion. To work around this glitch, just wrap the body of the script block in a CDATA delimiter:

 <script language="VBScript">
 <![CDATA[
   Function ShowMessage(s)
      MsgBox "The message is " & s
   End Function
 ]]>
 </script>
 Get Those Ones For Free…
      A couple of tags are supported silently by WSH 2.0. The <comment> and <resource> tags are not documented at the time of this writing, but work perfectly well. You've probably guessed that <comment> is an alternative way to insert comments in the WS file (as opposed to the typical <!-- --> delimiters).
      More interestingly, the <resource> tag isolates in separate blocks all the strings that you don't want to hardcode in the scripts. For example, add the following line in the <job> section of the code in Figure 3:

 <resource id="MSG_PREFIX">The message is </resource>
Next, replace the explicit string passed to the MsgBox function with this code:

 Function ShowMessage(s)
   MsgBox getResource("MSG_PREFIX") & s
 End Function
The final result is exactly the same, and you have a more manageable jobwide structure that contains all the user interface strings. (This is similar to the stringtables contained in a Windows-compliant executable.)
      These two additional tags are also supported by WS because the runtime module that takes care of parsing the WS code is actually the same one that parses the WSC—also known as COM objects written with script languages. (I plan to cover this topic in a future installment of Cutting Edge.) In fact, both <resource> and <comment> are documented tags for WSC.

New Command-line Switches

      WSH 2.0 is still run by two separate executables that output to the Win32® GUI and console mode, respectively. Wscript.exe and cscript.exe each support a few new command-line switches, as shown in Figure 5. The //E switch is particularly interesting since it addresses one of the most frequently asked questions about WSH 1.0: is it possible to run a VBScript file having any extension but .vbs? Now it is.



 wscript //E:VBScript myfile.xyz
You just have to specify the name of the invoked engine to run the code contained in the specified file. As shown earlier, the //Job option runs a specific job from a package. As a result, a WS file starts to look like a sort of script-based DLL where jobs play the roles of functions.
Figure 6: A WS File Opened in Visual InterDev
      Figure 6: A WS File Opened in Visual InterDev

      Since a WS file is an XML file, it gets free syntax highlighting by tools such as Visual InterDev (see Figure 6). But you can't simply open a WS file with Visual InterDev and have the syntax coloring for free; you have to switch to the HTML Editor to take advantage of this feature.

What's New in the Object Model?

      A number of new features make their debut in WSH 2.0. The structure of the object model remains mostly the same, but some methods have been added here and there. First, you now have a method to pause a script for a number of milliseconds. The following line stops the script's execution for half a second:


 WScript.Sleep(500)
In this time, the thread that's running the script is suspended without blocking the CPU. It'll be scheduled to resume automatically when the specified interval expires.
      Sleep is not a way to synchronize the script with other running applications. You can't use it to wait for a condition to occur, such as receiving a message or signaling one of the Win32 synchronization objects. Sleep just puts the script to sleep and wakes it up when the specified interval is expired.
      The Win32 SDK provides a SleepEx API function that supports an additional Boolean parameter called bAlertable in the documentation. Set it to True and the thread will awaken when events other than the timeout occur. See the MSDN documentation (http://msdn.microsoft.com/library/psdk/winbase/prothred_0o8o.htm) for more details. This feature is not provided by the current version of the Sleep method, but it might be included in the final release version.
      One of the most misunderstood WSH methods is DisconnectObject. It doesn't release the object it references, but simply unadvises any sink that's been registered to handle the object's events. If your script is connected to an instance of an object so it can process that object's events, after a call to DisconnectObject the event notifications will stop flowing. There was no ConnectObject in front of DisconnectObject. The connection could take place only in the context of the object's creation. With WSH 2.0 you can connect to the object's events at any time using the ConnectObject method:

 WScript.ConnectObject(pObject As Object,
       bstrPrefix As String)
The syntax is similar to that used by WScript.CreateObject. The bstrPrefix argument is mandatory.
      The WScript object has been enriched with the ability to manage text streams so it can provide better interaction by reading and writing with special devices such as stdin, stdout, and stderr. Note that this works only with the console version, CScript, and not with WScript. Here's an example of how to instantiate corresponding StdIn and StdOut objects:

 Set StdOut = WScript.StdOut 
 Set StdIn = WScript.StdIn 
The StdOut and StdIn members of WScript each return a TextStream object. You read from them with the ReadLine method and write to them with WriteLine. You can also check the end of the stream using the AtEndOfStream method.
      Another method demanded by developers is SendKeys. It has been built around the Visual Basic SendKeys statement, and you may refer to the Visual Basic documentation for more details about specifying keys. SendKeys is a WshShell method that allows you to send keystrokes to the application that gets the keyboard focus. This is a smart way to set up a very simple dialog with a Windows-based application that doesn't expose an object model.
      For example, to open Notepad, write a few lines, and print them, you can use the following code:

 Dim shell
 Set shell = CreateObject("WScript.Shell")
 shell.Run("Notepad")
 shell.AppActivate("Notepad")
 shell.SendKeys "Print this text"
 shell.SendKeys "%FP"
The % symbol denotes the Alt key, while the curly brackets (shown in the next snippet) specify a special key such as a function key. To close Notepad you could send Alt+F4

 shell.SendKeys "%{F4}"
but you'll get a message box asking you to save the text you entered. The parameter passed AppActivate is the caption of the recipient program's main window.
      The SendKeys syntax is

 WshShell.SendKeys (bstrKeys As String, [pvarWait])
where the second argument is a Boolean value that's set to False by default. If you set it to True, it waits for the application to process the keystrokes. Otherwise, it forces the method to return immediately.

File Version

      The nearly complete object model exposed by FileSystemObject previously lacked a method to read the version information of a given file name. Currently, many system behaviors are heavily affected by whether you're running Active Desktop, VBScript 5.0, or Microsoft Internet Explorer 5.0. An easy way to check the version is this welcome addition:


 set fso = CreateObject("Scripting.FileSystemObject")
 Msgbox fso.GetFileVersion("c:\windows\system\shell32.dll")
This code displays the version number of the Windows shell. The script in Figure 7 checks whether Active Desktop is installed (that is, whether the version number of the shell is not less than 4.71).
      Speaking of FileSystemObject, another noteworthy feature is the set of enumerated types that let you automatically import constants for the I/O modes and drive types.

Put WSH 2.0 Through its Paces

       Figure 8 summarizes what's new in the WSH 2.0 object model. If you're using WSH 2.0, it's very likely that you're using VBScript and JScript 5.x engines as well. Such a mix turns out to be very powerful since it lets you build clear and clean classes using VBScript 5.x, and track errors using the JScript-structured exception handling mechanism. For example, you might define a VBScript class in a single file and then import it in a WS script (see Figure 9). The following line imports the declaration of the class:


 <script language="VBScript" src="TickCounter.vbs" />
You can use it later via the VBScript 5.x new operator:

 Dim t
 Set t = new TickCounter
This is a great help for script developers. Classes make code more modular and structured, increasing reusability as well.
      There's another aspect of WSH 2.0 that I'd like to present in a different context: the <reference> tag. I've shown you how it can be useful with an object model like ADO. Well, you can design your own components that extend the WSH capability to integrate with the <reference> tag. Your components could expose enumerated constants that could be imported in a WSH script.
      To demonstrate this, I've written a very simple ATL COM component that attempts to do what SendKeys does, but better (see Figure 10). My component lets you execute two keyboard-related actions such as printing the screen and opening the Start menu (admittedly, not a very frequent need). The component has a progID of SendKeys.ExtKey and exposes just one method, Send. It takes a numeric constant as its input: 1 for PrintScreen and 2 for StartMenu. The keys to press are PrintScreen and Ctrl-Esc, respectively. I used the keybd_event Win32 function to obtain the result.
      Here are the C++ lines that send the keys to the buffer:

 keybd_event( VK_CONTROL, 0, 0, 0 );
 keybd_event( VK_ESCAPE, 0, 0, 0 );
 keybd_event( VK_CONTROL, 0, KEYEVENTF_KEYUP, 0 );
 keybd_event( VK_ESCAPE, 0, KEYEVENTF_KEYUP, 0 );
Remember that you have to first simulate the key being pressed and then its subsequent release.
      To define an enumerated type to distinguish the action, I entered the following IDL code in the ATL project:

 library SENDKEYSLib
 {
   importlib("stdole32.tlb");
   importlib("stdole2.tlb");
 
    typedef enum {
      PRINTSCREEN = 0x01,
      STARTMENU = 0x02
    } SpecialKeys;
To use these types of constants in WSH, simply reference the object. Notice the use of the version attribute.

 <reference object="SendKeys.ExtKey" version="1.0" />
From now on, in the script code you can use any of the special keys defined previously.

 <object id="sk" progid="SendKeys.ExtKey" />
 • • •
 sk.Send PRINTSCREEN
       Figure 11 illustrates how to call such an object from within a WS file. If you choose to print the screen, the content is immediately saved to the clipboard and then dumped into Microsoft Paint through SendKeys.

 shell.run("mspaint.exe");
 shell.AppActivate("untitled - Paint", true)
 WScript.Sleep(100);
 shell.SendKeys("^v{ENTER}", true);
Notice that I made use of both VBScript and JScript code in Figure 11.

Two Final Tips

      I'll close this column with two quick tips taken from my recent book, Windows Script Host Programmer's Reference (Wrox Press, 1999). Passing parameters to a script is always bothersome during the edit/debug cycle. On the other hand, WSH scripts often need to work with no input but a file name. Relying on shell drag and drop is great, but you can arrange your script startup code in a way that detects whether there are arguments on the command line and prompts for them if there aren't any.


 if (WScript.Arguments.Count = 1) then
    arg0 = WScript.Arguments.Item(0)
 else 
    arg0 = InputBox("Enter the argument:")
 end if
      The second tip is about VBScript and JScript objects. You can call a JScript object from within VBScript code and vice versa. What matters is that the object is created by a function written in the original language. Once you do so, though, just return the instance through a function, and a script in another language will perceive it as a regular COM object.

From the September 1999 issue of Microsoft Internet Developer.