Simplify access to command-line output
Windows Script Host (WSH) has made its mark by leveraging outside components. Noteworthy WSH scripts often use external tools, such as Windows Management Instrumentation (WMI), Active Directory Service Interfaces (ADSI), and other COM-enabled libraries and applications. One particularly large and useful family of specialty tools is the traditional command-line tools. Command-line tools are ready-made for administrators and are often useful when tools such as WMI aren't available.
However, leveraging command-line tools in WSH scripts is difficult. Although WSH's WshShell object lets you access command-line tools through its Run method, using this method has two significant problems. The first problem is that many of the command-line tools have console-dependent behavior and can perform erratically if they execute in the wrong command processor. The second problem is more significant: The Run method doesn't provide a way to capture output. Although the new WshShell Exec method in WSH 5.6 returns command-line data, this method has limitations. It's complex to use because you must monitor streams. If you use WScript, the Exec method requires a visible console window, and a window pops up every time the method executes a command. Another limitation is that not everyone has WSH 5.6 installed.
To resolve these problems without repeating code every time I want to use a console tool in WSH, I use a generic command-line wrapper. The command-line wrapper provides an easy way to use command-line tools in WSH scripts. It captures the output from commands, doesn't require a visible console window, and works on all modern versions of Windows, even without WSH 5.6.
You can use the command-line wrapper in any WSH script. Although I wrote it in VBScript, you can even use it in JScript code through a .wsf file. Let's take a detailed look at how the command-line wrapper opens the correct command processor, runs the tool, captures all the command output, then sends that output to WSH.
Opening the Correct Command Processor
As I mentioned previously, command-line tools can run erratically if the wrong command processor runs them. In Windows XP, Windows 2000, and Windows NT 4.0, you typically want to use cmd.exe, the full 32-bit command-line interface. In Windows Me and Windows 9x, you have to use command.com. To ensure that the script uses the correct command processor, the command-line wrapper uses %COMSPEC%, a system-provided environment variable that specifies the name of the default processor.
The command processor must exit when it finishes. For cmd.exe, command.com, and all other standard processors, the /c switch forces the processor to close. To open the correct command window on the local system, then close the command window after the tool has run, the command-line wrapper uses the code
%COMSPEC% /c cmd
where cmd is the command that launches the command-line tool. So, for example, if you want to run the Ipconfig command-line tool, the code would be
%COMSPEC% /c ipconfig
Ipconfig is a useful command-line tool in WSH scripts. Using other tools such as WMI to obtain a PC's IP configuration data requires many lines of code because of possible multiple instances of addresses, gateways, subnet masks, and other key information. Ipconfig, which is available on XP, Win2K, NT 4.0, Windows Me, and Win98 (and even Win95 with the Microsoft Windows 95 Resource Kit), acquires this data in one step.
Running the Tool
At this point, you have the code in place that opens and closes the command processor. Now you need to add code that tells the VBScript scripting engine to run the command-line tool. You can use WshShell's Run method, which has the syntax
object.Run(strCommand, \[intWindowStyle\], \[bWaitOnReturn\])
where strCommand is the command to run (in the command-line wrapper's case, the command that launches the command-line tool). The intWindowStyle argument tells the scripting engine what kind of window (if any) to display when running the command. The command-line wrapper specifies the value 0 for intWindowStyle so that the scripting engine hides the window. The bWaitOnReturn argument tells the scripting engine whether to wait until the command finishes running before continuing. For bWaitOnReturn, the command-line wrapper specifies the True value so that the scripting engine won't proceed to the next line of code until the command finishes running. Thus, at this point, the command-line wrapper looks like
cmd = "ipconfig" Set sh = CreateObject _("WScript.Shell") sh.Run "%COMSPEC% /c cmd", 0, true
Capturing All the Command Output
The next step is to add code that captures all the tool's output. Capturing and redirecting standard output is simple. If you have experience with writing batch files or running commands from the command line, you're probably familiar with using the > redirection symbol to send command output to a file. However, with command output, you can't just redirect the output to a standard file. You need to consider what happens if the command output doesn't come back as standard output. If the command process can't find the command-line tool or the tool encounters problems while running, the tool usually sends error output to a channel called the command error output stream. You'll lose the error output if you don't redirect the error output stream to a file.
When you use the > redirection symbol, you can specify numbers that correspond to the standard output stream and the error output stream. Preceding the > redirection symbol with the number 1 specifies the standard output stream, whereas preceding the > redirection symbol with the number 2 specifies the error output stream. (If you don't preface the > redirection symbol with any number, the output goes to the standard output stream by default.) A good practice is to redirect both the standard output stream and error output stream to temporary files to avoid accidentally overwriting other files and to easily erase the file when you're finished using it. So, the command-line wrapper uses the code
%COMSPEC% /c cmd 2>err.tmp 1>out.tmp
to redirect the tool's standard output to the temporary file named out.tmp and any error messages to the temporary file named err.tmp. Thus, the Run method now looks like
cmd = "ipconfig" Set sh = _ CreateObject("WScript.Shell") sh.Run "%COMSPEC% /c" & _ cmd & "2>err.tmp 1>out.tmp", 0, true
This code is fine if you plan to use the command-line wrapper in only one script. If you used the wrapper in more than one script running simultaneously, you'd run into problems because you'd be using the same two temporary files (err.tmp and out.tmp) to store the command output. To avoid this problem, you can use the FileSystemObject object's GetTempName method to generate random filenames for the temporary files. (The FileSystemObject object is part of the Scripting Runtime Library.) The generated filenames start with the letters rad, followed by five characters chosen from the set 0 through 9 and A through F (i.e., a hexadecimal number). Examples of filenames are rad35D8F.tmp and rad1986E.tmp. Before generating a new filename, the GetTempName method doesn't check to see whether that filename already exists. However, the name set has 1 million members, so you probably won't end up with a redundant filename. If you do, the problem will almost certainly be a script-generated temporary file that wasn't correctly erased.
To use the GetTempName method, the command-line wrapper has to include code that creates an instance of the FileSystemObject object as well as the code that calls this method into action. With this code in place, the command-line wrapper looks like the code that Listing 1 shows.
Getting the Command Output into WSH
After all the output is captured in a temporary file, you need to get that output into a text file that WSH can read. The ProcessFile function, which Listing 2 shows, does that and more. This function also avoids possible errors and performs housekeeping tasks. For example, the function checks for the existence of the temporary files before it tries to read those files. Under rare circumstances, a temporary file might be missing because the output stream is blocked. Trying to read a file that doesn't exist will generate an error. Trying to read an empty file will also generate an error. And you can't determine whether a file is empty from its size alone; an empty Unicode file can contain 2 bytes of data. So, the function checks to see whether a temporary file is empty before trying to read it.
The ProcessFile function also determines whether the temporary file's format is ANSI or Unicode. The file format varies depending on the system's configuration details. The function reads the proper format to avoid getting garbage back. Finally, the function cleans up by deleting the temporary files.
To make the command-line wrapper easier to use, I wrapped the code in Listing 1 and Listing 2 into a class named CliWrapper. Listing 3 shows this class.
How to Use CliWrapper
To use the CliWrapper class, you need to insert the code at callout B in Listing 3 anywhere in the script's global code. I usually place classes at the end of the script to make it easier to focus on the code instead of the class details. You then need to have the code at callout A in Listing 3 at the beginning of the script. After declaring Option Explicit and the console variable, this code creates a console object from the class. The code then executes the command-line tool and displays the result.
The demonstration code at callout A is currently set to execute Ipconfig. To have it run a different command-line tool, you simply customize the Exec method's argument. For example, if you want to use the Dir command-line tool to find all the .ini files in the Windows directory and its subfolders, you'd replace console.exec("ipconfig | Find ""IP Address""") with console.exec("dir %windir%\*.ini /s /b"). When you're specifying a new command-line tool, keep in mind the following tips:
Watch your double quotes. Using double quotes incorrectly is a common mistake in VBScript code. The VBScript scripting engine interprets a double quote as the end of a string; if you have to use a double quote for another purpose, such as to specify a file path that contains a space, you need to use the symbol twice to make VBScript interpret it as one literal double quote. So, for example, if you're looking for Microsoft Excel spreadsheets in the path E:\My Documents, you'd write the Exec argument as console.exec("dir ""E:\My Documents\*.xls"" /s /b /a-d"). For more information about using double quotes, see "Rem: Understanding Quotation Marks in VBScript," December 2002, http://www.winscriptingsolutions.com, InstantDoc ID 26975.
Filter command output. Sometimes, command-line applications give you a lot more information than you want. The simplest way to handle this situation is to pipe the command output to the Find command, which will return only the lines that contain the string you specify. In the Find command, you must enclose the search string in double quotes, which means that you'll need to enclose the string in two consecutive double quotes, as I just explained. For example, as callout A in Listing 3 shows, the Ipconfig output is being piped to the Find command, which will search for those lines that contain the string IP Address. The Find command has an /i switch that you can use if you want to ignore case in the search.
In addition to using the Find command, you can use VBScript's string-manipulation functions. Functions such as Left, Right, Mid, Split, and Join are extremely useful for processing console output.
Help. Almost every decent console tool has a command-line Help switch—typically /? or /h—that you can use to access information. In XP and Win2K, you can call up the general command-line Help with the following command at a command prompt:
In addition, the CliWrapper class has a couple of helpful mechanisms. It has a Command property that lets you see your command the same way Windows sees it. If something isn't running as you expect, try using the following statement to see the command exactly as executed:
WScript. Echo console.command
In addition, you can echo the script's error output using
WScript. Echo console.stderr
Finally, you can instantly reuse the console object by simply calling console.exec with a new command string. You don't need to add another New statement because the console will automatically clean up its old output each time it's called.
The Right Tool for the Task
The CliWrapper class makes console-application-output capture simple by hiding complex implementation concerns. We haven't addressed an important question, however: Is hiding these concerns a good thing to do? Obviously, the WSH designers thought so because in WSH 5.6, WScript.Shell's Exec method provides some support for this approach.
Command-line tools have a lot to offer. First, they're usually efficient and reliable. The command shell takes minimal overhead, and console tools typically perform only a handful of specific lightweight API calls. Because they don't need to display and scroll data, when these tools are wrapped up they can actually be faster than usual.
Second, console tools are long-lived. Some have been around for almost 20 years and no longer have any rough edges.
Third, console tools are well designed for administrative scripting. Generally speaking, administrative scripting tasks involve linear decision making and filtering, and data is focused on large-scale system parameters. In contrast, most of the scriptable COM objects are designed first and foremost for programmers and thus can require a significant amount of coding to make them do what you want.
Ultimately, to determine whether a command-line tool is appropriate for a particular subtask in a WSH script, you have to try it before you decide. After you do, you'll discover that command-line tools are often the best tools for the job.