Police the WSH command line
Windows Script Host (WSH) provides two useful but potentially confusing tools for working with command-line arguments: the WshArguments object, which lets scripters easily read supplied arguments and their types and values, and (specifically in Windows Script File—.wsf—files) the XML <runtime> element, which automates Help text generation. (A .wsf file uses an XML structure to add helper elements to a script; for easy identification, I refer to these helper elements by their tag names, which are simply the element names enclosed in the angle brackets—<>—used to delimit tags.) It's easy to confuse these tools' characteristics. Scripters often assume that the <runtime> element's <named> and <unnamed> elements validate user-supplied arguments, when in reality these elements simply give you nicely formatted Help text. As Bob Wells discusses in "Rem: Relating the WSH 5.6 <named> Element to Mandatory Arguments," October 2002, InstantDoc ID 26363, the elements don't validate user-supplied arguments. In other words, even though the <named> and <unnamed> elements in a .wsf script explicitly describe argument constraints (e.g., what's required, what's optional, what kind of values arguments should have, the number of arguments), you have to write the code to validate those arguments.
Although WSH simplifies validation, you still have room for mistakes and extra work. You might modify arguments used within a script but forget to modify the relevant Help descriptions. You'll probably validate arguments a little differently in each script, and you'll waste time writing similar code multiple times. Fortunately, you can use Microsoft XML components to parse the <runtime> tag contents fairly easily, so you can write code that will automatically validate user-supplied arguments against the runtime specification.
In this article, I discuss some details of the <runtime> element of .wsf scripts and the WshArguments collection and demonstrate (with the code in Listing 1) that WSH doesn't validate user arguments against the <named> and <unnamed> elements. I then walk you through using a Windows Script Components (.wsc) component that I wrote to automate .wsf argument validation. To demonstrate the component, I use a WSH script called tee.wsf, which reads characters from a command console's standard input stream and writes them to the standard output and to one or more files. At the end of the exercise, you'll know how to add as few as four lines to your scripts to automate argument validation using my component. You'll also have a handy utility for use with other scripts.
The <Runtime> Element and the WshArguments Collection
As I mentioned, you can document a .wsf script's supported arguments in its <runtime> section. (See http://msdn.microsoft.com/library/en-us/script56/html/wslrfruntimeelement.asp for more details about the <runtime> element.) A user can run the script with the command-line argument /? to display the Help text. Alternatively, the script can call the WScript.Arguments.ShowUsage method. I refer to the <runtime> section as the arguments specification because it's a construction that tells users the command-line requirements they need to meet to run the script.
The WshArguments collection, in contrast, tells you inside a script what arguments the user actually used. For detailed information about the WshArguments collection, go to http:// msdn.microsoft.com /library/en-us/script56/html/wsobjwsharguments.asp.
The Problem: Unvalidated Arguments
To understand the problem of WSH not validating arguments, look at the dummy script in Listing 1. If you save that script with a .wsf extension, then run the script from the command line with the /? parameter, you'll see the following Help text:
listing1.wsf /O\[+|-\] /C:value file1 \[file2...\]
For those who aren't familiar with the syntax for command-line arguments, square brackets (\[\]) denote optional items. The only exception is that items inside square brackets and separated by a pipe (|) symbol represent a choice that the user must make: In the command above, the user must use /O+ or /O-, he or she can't just use /O. Anything not enclosed in brackets is required. In other words, according to the Help text, the two simplest command lines valid for this script are:
listing1.wsf /O+ /C:value file1
listing1.wsf /O- /C:value file1
However, the script will run with any command-line arguments the user gives it because the argument specification has no ability to enforce any usage rules the Help text sets forth.
The Solution: A Validation Component
The simplest way to tie Help text display and argument validation together is to write code that automatically analyzes the XML elements in the <runtime> section, then tests the user-supplied arguments to ensure they match the XML description. I wrote the wshargex.wsc component, which Web Listing 1 shows, to do just that. To automate validation, you need to install wshargex.wsc, make sure your scripts are written as XML-compliant .wsf files, and call the component within your scripts to perform validation.
1. Install wshargex.wsc. Make sure that you have WSH 5.6 or later—you need it to use the Named and UnNamed arguments as separate collections in a script. In Windows Explorer, right-click a .wsc component and select Register. If the .wsc component is valid, you'll see a message similar to DllRegisterServer and DllInstall in C:\Windows\System32\scrobj.dll succeeded. If you need to register wshargex.wsc from a command line, you can use the following command:
regsvr32 /s /i:"full-wsc-path" %systemroot%\system32 scrobj.dll
where full-wsc-path is the path to wshargex.wsc on your system.
The /s switch prevents regsvr32 from displaying a dialog box (regsvr32 still returns an error to the command line if registration fails). Make sure you register a permanent local copy of the component; registration tells Windows where to find the component, and if you try to use the component from a network location, it might not be available later.
2. Write your .wsf scripts as XML code. The WSH documentation includes details about how to write XML scripts; see "Script Component Files and XML Conformance" (http://msdn.microsoft.com/library/en-us/script56/html/letxml.asp) for the details. Despite the name, the discussion applies to .wsf files as well as .wsc components.
At a bare minimum, the first line of your .wsf file must be
To allow extended European characters (probably a good idea), you should also specify a wider character set. I typically use the following as my first .wsf line:
<?xml version="1.0" encoding="ISO-8859-1"?>
Last, you should use CDATA tags to "hide" your script elements, as I show here and in the listings:
<script language="vbscript"> <!\[CDATA\[ ' code goes here \]\]></script>
The <!\[CDATA\[ (i.e., character data) sequence tells the XML parser to treat everything between it and the \]\]> sequence as literal characters. If you don't use the CDATA tag, the XML parser actually parses special characters such as the angle brackets and the ampersand (&) instead of leaving them alone as script characters. You can use the CDATA tag in any <description>, <example>, <comment>, <script>, <resource>, or <usage> element to keep special characters from being parsed as XML.
3. Use argument validation in your scripts. Listing 2 shows tee.wsf, which uses wshargex.wsc to validate arguments. Tee.wsf functions like the UNIX tee command, which reads characters from a command console's standard input stream and writes them to the standard output as well as to one or more files. Instead of writing a few lines of code to check the command-line arguments from the user, tee.wsf just passes them to the component for validation.
To use standard input and output streams, you must run the script with CScript explicitly. Just having CScript set as your default script host isn't enough to redirect the console input stream. (For a simplifying workaround, see "Convert WSH Scripts into Console Tools," March 2004, InstantDoc ID 41501.) You can use this workaround for any situation in which you want to look at the results of a command on screen and send them to a file at the same time.
The code at callout A in Listing 2 shows the script's argument specifications. I specify only two arguments; by running the script with the /? argument (a default in WSH), you can get the argument Help screen to display automatically. The first specification, the named argument "A", tells tee.wsf to append to files instead of overwriting them. The argument type is simple, meaning that it should have no data. The second specification is an unnamed argument description called "filename" in the displayed Help text. This argument is the path to one or more files to which the console will send output. The user must specify at least one file.
The code at callout B begins the validation. First, I use the standard CreateObject command to set argEx as a reference to the WshArgument.Extensions class from wshargex.wsc.
Next, I give argEx a reference to WScript by setting its Wsh object to the internal WScript reference available to all WSH-hosted scripts. ArgEx needs this reference so that it can look up information such as the path to the script host, the path to itself, and the script argument collections.
As I mentioned earlier, the user must run the script with CScript, so the third line in callout B sets argEx's RequireCScript property to True. This setting makes validation fail if the tee.wsf script runs from the wrong host. If you try running tee.wsf with wscript.exe as the host, you'll see that tee.wsf automatically displays its Help text, then exits. If you try to supply a filepath as a value to the A argument (which must have no value), as in
cscript tee.wsf /a:c:\netstat.txt
tee.wsf displays Help and exits. If you run tee.wsf with no arguments, as in
tee.wsf displays Help and exits. If you run tee.wsf with one or more filenames, as in
cscript tee.wsf 1.txt 2.txt
the script just displays a blinking cursor on screen and echoes back everything you type until you press Ctrl+C. This behavior is expected because CScript is using your keyboard for standard input. When you press Ctrl+C, you might see an application error message; just press Enter to continue.
Next, tee.wsf calls the argument validation code in wshargex.wsc so that it can check the arguments. The validation (which ensures the arguments are the correct type and the host is appropriate) automatically occurs in the lines of code in callout B that I've described. ArgEx has several other properties that you might need to set for special behavior in a script; they're commented out in tee.wsf because it doesn't need them:
JobId. If you typically write .wsf files that contain multiple jobs, you need to specify the value of the id attribute from the .wsf file's <job> tag. Each job has to have its own <runtime> element, so you need to let the component know which job it's checking. To avoid worrying about this value, write .wsf files that contain only one job.
TraceExecution. TraceExecution works only when you use CScript as the host application. Setting this property to True tells argEx to echo tracing information as it runs. If you can't make some command-line options work, the trace will help you determine which argument caused argEx to fail and why.
ContinueOnFailure. If you set ContinueOnFailure to True, argEx will continue to check arguments even after it finds an invalid one. To determine whether validation failed, you can look at the return value of ValidateArguments or you can query the IsValid property after calling ValidateArguments.
AllowLooseTyping. This property turns off the type checking of named arguments. If you want to let users ignore the argument specification type of the named elements, set this property to True.
ExitErrorLevel. By default, if wshargex.wsc exits because of an argument validation error, the console error level returned is 2. You can use ExitErrorLevel to set the error level for failures to another value.
As I mentioned previously, I commented out these properties. If you want to set them, you need to do so before tee.wsf calls ValidateArguments, as callout B shows.
Where to Go from Here
As mundane as the topic of argument validation is, I actually found myself getting pretty excited when I saw how to automate it. When I use argument validation in this way, it frees me from worrying about a cumbersome and boring detail in scripting. Furthermore, it transforms Help text from an afterthought to a contract between the scripter and the script user—and the script itself will comply with it. If you want a script argument to work, you absolutely must write the argument specification.
You can see the potential of contracts like this by looking at what contractual interfaces did for programming a few years ago. After Microsoft introduced COM objects into Windows programming, they quickly became black-box tools. Their interfaces describe what kind of data they need and prevent using any other data types. Although this approach requires more programming discipline, the effect is that people no longer need to worry about the internals of a program element. They can look at its interface and be sure that everything they need to know about the data supplied to the component is right there. Checking arguments with my .wsf argument validation component provides the same guarantee that what you say the script wants is what it really expects; the simpler coding is just a fringe benefit.