Tales from the Script - October 2002
Running Programs From WSH Scripts
The Scripting Guys would like to welcome you to our new monthly column. we'd like to do that, but we can't. You see, it isn't our column at all; it's your column. You send us tons of great e-mail at firstname.lastname@example.org (in English, if possible) and this column is, and will continue to be, built upon your questions and comments. So, without further ado, the Scripting Guys would like to welcome you to your column; we sincerely hope you find it enjoyable and useful. If you don't, you know how to reach us!
One of the more-commonly asked questions we get is how to run programs from WSH scripts. Here's a typical snippet from one of those email messages.
On the webcast you mentioned that there are two lines of code that enables you to run an external program. Do you have a sample of that?
In response, we've decided to devote this first column to looking at how you can run programs from your scripts. Why would you want to do that? we'll, think of it this way: command-line tools enable you to perform a certain set of tasks and batch scripts provide the logical thread you can use to stitch those tasks together to create custom solutions. But what if the logical thread of batch scripts is not strong enough or a necessary component of a solution isn't provided by a command-line tool? That's where WSH scripts come into play. They enable you to use the strong logical thread provided by scripting languages like VBScript and they enable you to implement your own task solutions by using powerful tools like WMI and ADSI as well as command-line tools and batch scripts.
Let's get started right away by writing a script. Actually, before we do that let's make sure that you have a place to save your scripts. If you don't already have a folder called C:\Scripts, go ahead and create one. After you've done that, open up Notepad, or another text editor, type in the following script, and save it in your C:\Scripts folder with the file name RunIPConfig.vbs.
Set objShell = CreateObject("WScript.Shell") objShell.Run "ipconfig"
Before we try to run our new creation, let's examine it and try to figure out in advance what it's going to do. There are a couple of hints within the script that will help us out. The first line includes the verb Create (actually CreateObject) and the second line includes the verb Run. So, perhaps the script Creates something, called an object, and then Runs something. In fact, it looks like it runs the command-line tool, ipconfig.exe.
That's exactly what it does. The script first creates something called an object. In this case the object that is created is the Shell (WshShell) object. You can think of an object as being somewhat like a command-line tool; your script can use it to do a certain category of tasks. The Shell object enables you to do tasks from your scripts that you could do within the Windows shell – like run programs.
Unlike a command-line tool, you must create a new object before you can begin using it. When you create an object, you assign it a name that will be used to refer to it throughout the rest of the script. In this case, the name we used is objShell. After the object is created and it has an associated name, we can use the object to do tasks by following its name with a dot (.) and a name associated with the task we want to accomplish. In this case, that name is Run. Run is known as method of the Shell object.
Now open a command-prompt window and navigate to the C:\Scripts folder. To run your script, type cscript RunIPConfig.vbs and then press ENTER. You should see a window flash on the screen, but it disappears before you can see what was displayed in it! You could try running the script over and over again, trying to decipher what's being displayed in the window. This might even be fun, but there is a better way.
In order to understand the better way, we have to look into what is happening when you run a command-line tool. When you type ipconfig at the command-prompt and hit the ENTER key on your keyboard, you are giving an instruction to a program known as the command-interpreter. The command-interpreter examines what you typed. If it's a valid program, then the command-interpreter runs that program for you. If it isn't a valid program, the command-interpreter will let you know by displaying an error message. The command-interpreter on Windows NT-based systems is Cmd.exe. On Windows 9x systems, it is Command.exe.
Now, in the above script, the command objShell.Run "ipconfig" is bypassing the command-interpreter when it runs ipconfig; in other words, it is directly running Ipconfig, without first running the command-interpreter and then having the command-interpreter run Ipconfig. In some cases, this might be fine, but in this case, we don't want to bypass the command interpreter. Why? Because the command interpreter can help us out with our flashing window problem. You see, the command interpreter accepts a number of switches (try searching for cmd.exe or command.exe in your computer's help system if you are interested in finding out what they all do) and one of them — /k — instructs the command interpreter to keep the output window open after running a program. We want to use this switch so that the window doesn't open and then immediately close again.
To do so, we could change the command we are running from ipconfig to cmd.exe /k ipconfig or command.exe /k ipconfig depending upon whether we are running on a Windows NT-based or Windows 9x system. But what if you have to support both Windows NT-based and Windows 9x computer's? it's a hassle to keep track of which type of operating system we're using. For that reason, there's an environment variable, %COMSPEC%, that provides the full path to the appropriate command interpreter. That environment variable enables the following script to work on either Windows NT-based or Windows 9x systems:
Set objShell = CreateObject("WScript.Shell") objShell.Run "%COMSPEC% /k ipconfig"
Try running this modified script. Simply use Notepad (or a text editor of your choice) and add %COMSPEC% /k to the previous script, saving it as RunIPConfig.vbs. Then navigate to the C:\Scripts directory and run the script by typing cscript RunIPConfig.vbs and then pressing ENTER. This time you should see a new window displaying the output of the ipconfig command, as in the following screenshot.
The next thing you should do is experiment. This script can run much more than just ipconfig. All you need to do is modify the string following Run on the second line. Instead of %COMSPEC% /k ipconfig, try %COMSPEC% /k netstat, or %COMSPEC% /k mmc. You can run GUI tools from a script; for example, this script runs Calculator:
Set objShell = CreateObject("WScript.Shell") objShell.Run "Calc.exe"
You can run other scripts from within a script. You can also run batch files; if you happen to have some, try running them. Of course, if the location of your batch files isn't in the PATH environment variable on your computer, you will need to either add the location to the path or specify the complete path to the batch script. For example, if you have a batch script called Checkserver.bat in the C:\Batchscripts directory, then the string you pass to Run would look like: %COMSPEC% /k C:\batchscripts\checkserver.bat.
Assuming you're finished experimenting, we'll continue. You've learned how to use Run to run a single program. What if you want to run two programs in succession? That's what the following script does: it runs Nslookup followed by Ping. Notice that you can include command-line parameters for the program you are running in the string that you pass to Run. We're passing 127.0.0.1 as a parameter to both Ping and Nslookup.
Set objShell = CreateObject("WScript.Shell") objShell.Run "%COMSPEC% /k ping 127.0.0.1" objShell.Run "%COMSPEC% /k nslookup 127.0.0.1"
Save and run this script just as you did with the previous scripts. The result should be that two command-prompt windows open, with Nslookup running in one of them and Ping running in the other simultaneously, as in the following screenshot.
In this example, the fact that Ping and Nslookup run simultaneously is not a problem. However, what if you have two batch scripts you want to run and you need the first one to complete before the next one starts? Perhaps, for example, the first program retrieves information of some type and saves it in a text file; the second program, in turn, reads through that text file and takes action based on the contents. In such a case, having the two programs run simultaneously is not acceptable; the second program cannot begin until the first program has finished.
So far, we've been passing Run a single string that specifies the program that we want to run. But Run can accept two more parameters, each of which is optional. The second parameter let's you specify how you want a programs window to look when it is first run. Not all programs will use this parameter, but for those that do, it enables you to start a program with various window styles, including minimized, maximized, and hidden. We're not going to explore this parameter in this column. If you're interested in learning more about it, check out the WSH documentation available online at http://msdn.microsoft.com/en-us/library/dd575521.aspx (click on the Windows Script Host Documentation link near the top of the page).
The third parameter accepted by the Run method will enable us to prevent the programs from running concurrently. The third parameter can take a value of True, which indicates that the script should wait on the line where the Run method is used until the invoked program finishes running. We already know what happens if you don't specify this parameter, it defaults to false and you end up having programs run simultaneously.
Let's modify our previous script to run Ping, wait until it is finished, and then run Nslookup.
Set objShell = CreateObject("WScript.Shell") objShell.Run "%COMSPEC% /k ping 127.0.0.1",,True objShell.Run "%COMSPEC% /k nslookup 127.0.0.1"
Note that we've added a third parameter with the value True to the Run method called on the second line of the script. We had to use two commas because, if we used only one, then Run would think we're trying to specify a second parameter instead of a third. There's nothing between the commas, so nothing is passed to Run as it's second parameter. If you don't specify a value for a parameter, then the default value is used. For instance, the default value of the third parameter is False.
Try running the modified script. You should notice right away that only Ping runs. After Ping finishes, the window in which it ran just remains on the screen; Nslookup does not start. Try closing the window and see what happens. After you close the window, Nslookup starts. Remember that you are running cmd.exe (or command.exe) with the /k switch and you've told Run that it shouldn't proceed until cmd.exe is finished. Well, because of the /k switch, cmd.exe isn't finished until the window it's been asked to keep open is closed.
If you did have two batch files to run and you didn't need to see their output, you could remove the /k switch and they would still run consecutively instead of simultaneously, which is exactly what you want if the second program relies upon something the first program does.
It's one thing to specify that a program be run and another to know that it has actually happened. Some programs return an error code (a number) to indicate whether they've run successfully or not. Run can return that error code to your script. The program being run has to finish running before it knows what error code to return, so you must force the script to wait for the program to finish by setting the third parameter of Run to True.
In the following script, we run Ping and display the error code it returns by using the Echo command of the WScript object. The WScript object does not have to be created like the WshShell object; you can just use it's methods at any point within a script.
Set objShell = CreateObject("WScript.Shell") iErrorCode = objShell.Run("ping 127.0.0.1",,True) WScript.Echo iErrorCode
The script above differs from the previous scripts in a couple of ways beyond the fact that we are now retrieving the error code. First, notice that parentheses are around the string being passed to Run. This is because Run is now being asked to return a value. This is a VBScript language requirement; if you don't include the parentheses, you'll get an error when you run the script. The value being returned by Run is Pings error code and is stored in the variable iErrorCode and then displayed by using WScript.Echo.
The second thing to notice is that we didn't run the command interpreter. We want the error code from Ping, not from Cmd.exe (or Command.exe), so we run Ping directly. The error code returned from Ping is not interesting or useful. However, some command-line tools return useful error codes that you can use to make decisions (or create logs) within your scripts.
The information you can get back from a program that you run by using Run is minimal and might not be useful. For example, many programs simply return a value of 0 to indicate that they ran and they finished. However, they don't necessarily tell you that they ran and successfully finished.
However, if you have WSH version 5.6 (which you can get at http://msdn.microsoft.com/scripting), you have another method available for running programs that provides you with a better line of communication with the programs you run. That method is Exec. It is also a method of the WshShell object, the same object we had to create to use Run. What this means to us is that the first line of our script doesn't have to change to start making use of Exec.
Let's jump right in with a script that runs ipconfig by using Exec.
Set objShell = CreateObject("WScript.Shell") objShell.Exec "%COMSPEC% /k ipconfig"
The only difference between the script above and the second script we showed you is that we changed objShell.Run to objShell.Exec. Yet, when you run this script, it appears to do nothing. You don't even see a window quickly flash open. Instead you see nothing at all. Would you believe us if we told you that ipconfig was actually being run? Well, don't take our word for it, instead, add a redirection symbol (>), followed by a text file name after the call to ipconfig. That will cause the output of the command to be put in the file. If you're not used to redirection, think of it this way: if we're telling you the truth and ipconfig is running, then it is creating it's standard text display. Unfortunately, we can't see it. The redirection will allow us to capture any output and force it into a file. This will help us verify whether or not the ipconfig command is being run. This next script rounds up our output for us.
Set objShell = CreateObject("WScript.Shell") objShell.Exec "%COMSPEC% /k ipconfig > ipconfig_output.txt"
After running the script above, look at the contents of the ipconfig_output.txt file. You can do this by using the type command as we've done in the following screenshot.
So, there you have it! Our script is actually running ipconfig, it just isn't letting us look at the output. Believe it or not, that is actually a good thing. It's a good thing that Exec isn't showing us the output because we haven't asked for it. You see, with Exec, you are in control of how the output is presented and manipulated.
Remember how Run could return an error code? Well, Exec can actually return a whole object, known as a WshScriptExec object. Just as the WshShell object gave us access to the Run and Exec methods, the WshScriptExec object gives us access to additional functionality that allows us to, among other things, work with a programs output. As with many things in scripting, the additional functionality comes at the price of additional complexity. We're just about to leap into some of that complexity, so stay with us and stay focused during the next section.
The Exec method can return a WshScriptExec object. Making that happen in a script involves using the VBScript Set keyword and looks something like the following:
Set objWshShellExec = objShell.Exec(ipconfig)
Note: we've dropped the %COMSPEC%; it's not really needed because we don't want to pass the /k to keep a non-existent window open. After all, how would we close it to proceed? We could use the /c switch (which just runs a command and continues) and, in fact, we need it if we want to run a command that's built into the command interpreter (like dir, for instance). Tools such as dir are not programs; they are more like parameters to the cmd.exe (or command.exe), so you need to specify %COMSPEC% to make them work.
After you have a reference to the WshShellExec object stored in the objWshShellExec variable, you are almost ready to start working with the programs output, but not quite. Here's where the complexity kicks in! Just like the Exec method returned the WshShellExec object, the WshShellExec object can return another object; it is this object, returned by calling the StdOut property, that allows you to work with a programs output.
This is nearly impossible to understand without a sample script. So, here's the simplest example of using the output from a spawned program within a WSH script. Think of it as a special type of redirection. only this time it's not to a file but to a variable named strOutput instead. And, unfortunately, instead of a simple redirection symbol (>), we have to work with a couple of objects.
Set objShell = CreateObject("WScript.Shell") Set objWshScriptExec = objShell.Exec("ipconfig") Set objStdOut = objWshScriptExec.StdOut strOutput = objStdOut.ReadAll WScript.Echo strOutput
The output from the above script is displayed in the window from which the script is run; no new window is created. In fact, the output looks exactly the same as if you ran ipconfig itself from the command prompt. So, why not just do that? Well, we've created an intermediate holding place for the output in our script, the strOutput variable. Although we are immediately displaying that output on the next line of the script above using WScript.Echo, we could be doing something to the output before displaying it, thus changing the information the ipconfig tool displays.
Before we get into that though, let's have a closer look at the last three lines of the script; the first two lines are already familiar to you. In the third line, the StdOut property of the WshScriptExec object is used to retrieve an object that represents the (standard) output of the program being run; ipconfig in this case. In line four, the ReadAll method is used to retrieve all of the output and store it in the strOutput variable. Lastly, in line 5, WScript.Echo is used to display the output.
We now have what is often referred to as a wrapper around the ipconfig tool (or any other tool we substitute for ipconfig in the script above). A wrapper is a program that uses the functionality of another program and makes it convenient to modify what the wrapped program can do. The wrapper gives us an opportunity to modify the wrapped programs output. That's what we will do in the remainder of this article.
On our way to customizing ipconfig, we will need to learn how to work with the programs output a line at a time. We're currently using objStdOut.ReadAll to retrieve all of ipconfigs output and store it in the strOutput variable. What we'll do in the next script is retrieve the output line-by-line by using ReadLine instead of ReadAll. We'll examine each line and if it includes the string "Physical Address", we'll display it; otherwise, the line will not be displayed. We're just filtering for the line that includes "Physical Address" (which shows the MAC address of a network card), and not displaying anything else.
Set objShell = CreateObject("WScript.Shell") Set objWshScriptExec = objShell.Exec("ipconfig /all") Set objStdOut = objWshScriptExec.StdOut While Not objStdOut.AtEndOfStream strLine = objStdOut.ReadLine If InStr(strLine,"Physical Address") Then WScript.Echo strLine End If Wend
The first three lines of the script above should be somewhat familiar; note that we had to add the /all switch to ipconfig to get the "Physical Address" to be displayed. A major difference between this script and the last one is the While/Wend loop. We're using the ReadLine method to retrieve individual lines, but we needed some way to know when We're out of lines to retrieve. That's where the While/Wend loop and the objStdOut.AtEndOfStream come into play. The loop will continue (and lines will be stored in strLine) as long as we have not reached the end of the (standard) output stream. You can think of a stream as a long series of characters that you are working with sequentially; in this case, the entire output of ipconfig /all.
Each time we pass through the loop, we store the next line in strLine and then use an If/Then statement to determine whether or not to display the line. We use the InStr (in string) function to find out if the line currently in the strLine variable includes "Physical Address". If it does then we use WScript.Echo to display it. The following screenshot shows some typical output from the script.
Our next script will use the same basic elements as the previous one, but this time, instead of filtering the output, we will add to it. Since ipconfig /all retrieves information about when a DHCP lease was obtained and when it will expire, it might be useful to have the current date and time included in the commands output. The next script does just that.
The script will begin by retrieving and displaying the first four lines, unaltered, by using calls to ReadLine immediately followed by calls to WScript.Echo. After the fourth line has been retrieved and displayed, we will construct a string that includes a label (Current Date/Time) and the current date and time as returned by the VBScript Now() function.
Set objShell = CreateObject("WScript.Shell") Set objWshScriptExec = objShell.Exec("ipconfig /all") Set objStdOut = objWshScriptExec.StdOut ' ' Skip first four lines ' strLine = objStdOut.ReadLine WScript.Echo strLine strLine = objStdOut.ReadLine WScript.Echo strLine strLine = objStdOut.ReadLine WScript.Echo strLine strLine = objStdOut.ReadLine WScript.Echo strLine ' ' Add date/time information ' strCurrentTime = " Current Date/Time. . . . . . . . .: " & Now() WScript.Echo strCurrentTime ' ' Display the rest of the output ' While Not objStdOut.AtEndOfStream strLine = objStdOut.ReadLine WScript.Echo strLine Wend
Admittedly, this is starting to look a little complicated. The best way to approach it is to look at it in sections. The first three lines are familiar by now. The next commented section uses ReadLine and Echo to read and display the first four lines of output from the call to ipconfig /all.
The next commented section begins with the construction of the new information line that will be temporarily stored in the strCurrentTime variable. The line is constructed from a simple string concatenated (combined) with a call to the VBScript Now() function, which returns the current date and time. Once the information line has been constructed and stored in the strCurrentTime variable, we then use WScript.Echo to display the line.
In the last commented section, we loop through the rest of the output, displaying it line-by-line without any alteration. The following screenshot shows typical results from running the script. Notice that the output looks just like the standard output from ipconfig. The Current Date/Time line we inserted below the Host Name line blends in seemlessly with it's new surrondings.
To finish off, let's clean things up a little by using a For/Next loop to output those first four lines, as shown in the following script.
Set objShell = CreateObject("WScript.Shell") Set objWshScriptExec = objShell.Exec("ipconfig /all") Set objStdOut = objWshScriptExec.StdOut ' ' Skip first four lines ' For i = 1 To 4 strLine = objStdOut.ReadLine WScript.Echo strLine Next ' ' Add date/time information ' strCurrentTime = " Current Date/Time. . . . . . . . .: " & Now() WScript.Echo strCurrentTime ' ' Display the rest of the output ' While Not objStdOut.AtEndOfStream strLine = objStdOut.ReadLine WScript.Echo strLine Wend
There is actually another way of running programs from WSH scripts: you can use Windows Management Instrumentation (WMI). In fact, you can use WMI for a huge range of Windows management tasks! Using WMI to run programs involves slightly more complicated syntax but enables you to do things that you can't do with Run or Exec, like set the priority with which the program will run or run the program on a remote computer. We really recommend you learn more about WMI. If you're interested, and we hope you are, check out our three-part series in our other column, Scripting Clinic, over on MSDN (http://msdn.microsoft.com/columns).
Well, hopefully we've answered some of the questions you had about running programs from WSH scripts. we've given you a quick overview of Run and Exec and touched on the differences between them, the most obvious one being that Exec enables you to work with the output from the program whereas Run doesn't. Another important difference: Exec is only available with WSH version 5.6 or later.
We hope you've enjoyed your first column. Please write to us at email@example.com (in English, if possible) and let us know. And don't forget to tell us what you want covered in future columns. If you have time, check out the Scripting Clinic WMI series and if you end up doing something neat and useful with what you've learned here, drop us a line; we'd love to hear about it!