July 2004
For a list and additional information on all Tales from the Script columns, click here.
On This Page
Scripts Like a Good Argument
Adding command-line arguments to a script
So what are command-line arguments again?
That’s cool, but it would be even cooler if we could….
One is the loneliest number
What about any service on any number of computers?
But, uh, what about any service on any number of computers?
Where do we go from here?
Scripts Like a Good Argument
Most of you have probably heard of periodic cicadas, more commonly known as 17-year locusts. Periodic cicadas are insects that show up one summer, make a bunch of noise, cause a big commotion, and then disappear from sight, only to re-emerge 17 years later and repeat the cycle all over again. It’s probably fair to say that, in a lot of ways, we’re as much the Microsoft Scripting Cicadas as we are the Microsoft Scripting Guys: we show up, make a bunch of noise, cause a big commotion, and then disappear. And then suddenly, 17 or so years later, we’re back. Will the cycle repeat itself? Who knows? Log on to TechNet in the year 2021 and find out!
In other words, yes, we know: it has been a while since the last Tales from the Script column. There are a lot of reasons for that—wait, who said, “Starting with the fact that you guys are just plain lazy?” Why, if we weren’t so lazy we’d really give you a…. But remember, the important thing is not “Where have you guys been?” The important thing is that we’re back, and that we’re back as part of the all-new, all-cool Script Center. At the time of this writing, we aren’t sure how many of these new features have actually made their debut, but you’ll soon see an expanded Script Center that includes:
Hundreds of new scripts.
Links to Webcasts, scripting articles, scripting columns, and more.
A new Solutions Center, featuring enterprise-ready scripts developed by Microsoft Product teams.
The Script Writer’s Toolkit and the Script Writer’s Bookshelf, annotated lists of scripting tools and resources.
A new daily question-and-answer column, Hey, Scripting Guy!
And on top of that, we’ll have a new Tales From the Script column for you every 17 years. Guaranteed!
Oh, right. OK, then, we’ll have a new Tales from the Script column for you every month, once again making Tales from the Script the first place scripters should turn to to find simple, easy-to-understand answers to common scripting questions, like “How come it takes you guys so long to write one little scripting column?”
Adding command-line arguments to a script
What we thought we’d do this month is talk about how you can use command-line arguments in your scripts. (Yes, that is a good idea for a column, but you don’t need to thank us; after all, we did have 17 years in which to come up with a topic.) As you’ve probably noticed, most of the scripts in the Script Center are designed to run against a single computer; not only that, but we also tend to hard-code things like computer names. For example here’s a script that does one thing and only one thing: it stops the Alerter service on the local computer. If you’ll look closely, you’ll see that we’ve specified both the name of the computer the script should run against (.) and the name of the service to be stopped (Alerter):
strComputer = "."
Set objWMIService = GetObject _
("winmgmts:\\" & strComputer & "\root\cimv2")
Set colServices = objWMIService.ExecQuery _
("Select * FROM Win32_Service WHERE Name = 'Alerter'")
For Each objService in colServices
objService.StopService
Next
Needless to say, having a script that does nothing but stop the Alerter service on the local computer doesn’t seem particularly useful or exciting. So then why do we do write scripts like this? Well, there are at least two reasons. For one, hard-coding values helps keep the scripts short. Our scripts are designed to be educational, and we don’t want to clutter them with a lot of extra code; we want to make it easy for you to zero in on the task at hand. That’s why we don’t usually include error handling in our scripts, either; for our purposes, the simpler the script the better. (Besides, look how long it takes us to write simple scripts; can you imagine how long it would take us to write more complicated scripts?)
For another, keeping the scripts simple makes it easier for you to take our scripts and modify them as needed. For example, suppose we wrote all our scripts so that they read in computer names from a text file, performed some task, and then output the data to Excel. There’s nothing wrong with that; however, it’s not easy to take that kind of script and modify it to pull computer names from Active Directory and then echo information to a command window. The more complicated we make the scripts, the harder it is for you to customize those scripts to meet your unique needs. In turn, that means we start to drift into the realm of solution providers as opposed to educators, and—trust us—nobody wants that. (Unless, of course, you don’t mind waiting 17 years for a solution to be provided.)
In general, this has been an effective approach. However, we have to admit that we do have a bad habit of saying things like “Sure, this is a pretty barebones script, but you can easily modify it to do things like accept command line arguments.” That’s true, only we never seem to get around to telling people how to modify our sample scripts so that they will do things like accept command line arguments. But that’s an omission we’re going to take care of right now. (And you guys said we were lazy….)
So what are command-line arguments again?
Just to make sure we’re all on the same page, a brief definition is in order. Command-line arguments are additional information passed to a utility (could be a script, could be an executable file, could be whatever) when you start that utility. For example, suppose you try to ping the IP address 192.168.1.1. In that case, you’re going to type something like this from a command prompt:
ping 192.168.1.1
As you might have guessed, the 192.168.1.1 is a command line argument (sometimes called command-line parameters, sometimes called command-line switches, sometimes called just plain old arguments). Why do you even need command-line arguments? Well, you don’t. But take Ping, for example. Suppose you couldn’t pass the IP address as a command-line argument; what good would Ping do you then? Ping is a useful utility because it allows you to ping any IP address; it can do that because it doesn’t have a single IP address hard-coded in. Instead, you specify the IP address as a command-line argument each time you run Ping. Without the ability to accept command-line arguments, Ping would be of limited use, to say the least.
The same thing is true of your scripts. If you hard-code in the name of one computer, then your script will only act against that computer. What if you want the script to run against a different computer? Well, then you’ll either have to modify the script or you’ll have to create a brand-new script that runs against this second computer. Needless to say, neither way is particularly efficient. A better approach would be to create a single script that includes an easy way to make that script run against any computer. And that’s the whole idea behind command-line arguments.
We know that some of you are starting to panic here; you’re thinking, “Command-line arguments? That sounds hard. Look, it’s only been 17 years; couldn’t you leave us alone a little while longer?” Listen, there’s no reason to worry; as you’ll see, command-line arguments are pretty easy to deal with. In fact, you can pass command-line arguments to your scripts right now. Type this one-line script into Notepad and save it as args.vbs:
Wscript.Echo strComputer
Now run the script from the command prompt using this command:
cscript args.vbs atl-ws-01
And what happens? Well, OK, nothing, but if you’re a Tales from the Script reader, you’re obviously used to that. Why doesn’t anything happen (beyond getting a blank message echoed to the screen, that is)? That’s easy: the variable strComputer isn’t set to anything. But don’t worry about that for now. The important thing is that you passed a command-line argument (atl-ws-01) to your script, and your script accepted it without any problem. In other words, Windows Script Host accepts command-line arguments without any special coding; the only thing you have to do is include a line or two of code that enables the script to make use of those arguments.
Incidentally, when you started this script and passed it a command-line argument, you might have wondered, “So what did happen to that argument? Did it just disappear?” The answer is no, it didn’t just disappear. Instead, all command-line arguments passed to a script are stored in the WSH Arguments collection, a collection that is automatically created for you each time you run a script. The Arguments collection is nothing more than an array consisting of all the command-line arguments passed to the script at run time. When you started the script, the argument atl-ws-01 actually got stored in the Arguments collection; it’s just that we didn’t have any code in our script that retrieved the value from the collection. But if you give us a second, we’ll show you how to add that kind of code to your script.
Note Before we get much further, we should point out that, in the world of WSH, arguments are delineated by spaces. Suppose we type this:
In that case, we have two arguments: atl-ws-01 and atl-ws-02. Suppose we type this:
How many arguments do we have here? That’s right; it’s just one big argument: atl-ws-01/atl-ws-02/atl-ws-03. That’s because individual arguments must be separated by blank spaces. To pass three arguments, we need to type this:
What if you type in more than one space between arguments, like this?
Hey, no problem; WSH automatically discards the extra spaces. But what if you need spaces as part of an argument, like when trying to pass the folder name C:\Documents and Settings? In that case, just surround the argument with double quote marks, like so:
|
OK, so what happened to our argument again? As we said, arguments are stored in an array, and, like most arrays, this one assigns an index number to each item in the array: the first item is assigned index number 0, the second item is assigned index number 1, and so on. As far as the script is concerned, the array looks like this:
Index No |
Value |
---|---|
0 |
atl-ws-01 |
1 |
atl-ws-02 |
2 |
atl-ws-03 |
Why do you care about that? Well, believe it or not, you now know how to access command-line arguments. Take a look at this script:
strComputer = Wscript.Arguments.Item(0)
Wscript.Echo strComputer
Type the preceding script into Notepad, save it as args.vbs, and then run it using this command:
cscript args.vbs atl-ws-01
Guess what happens? This time, the script echoes back the value atl-ws-01. Not only that, but it will echo back any argument you pass it. Don’t believe us? Start the script using this command and see for yourself!
cscript args.vbs this_is_my_command_line_argument
For that matter, start the script using any command-line argument and see for yourself. Works every time.
That’s why we told you not to panic; this is way too easy. If you look at the script, you’ll see that the first line of code takes the variable strComputer and assigns it Wscript.Arguments.Item(0). What is Wscript.Arguments.Item(0)? That’s the first item (the item with index number 0) found in the Arguments collection. In other words, strComputer gets set to the very first argument we typed in. You want to create a simple script that can run against any computer? Here you go:
strComputer = Wscript.Arguments.Item(0)
Set objWMIService = GetObject _
("winmgmts:\\" & strComputer & "\root\cimv2")
Set colServices = objWMIService.ExecQuery _
("Select * FROM Win32_Service WHERE Name = 'Alerter'")
For Each objService in colServices
objService.StopService
Next
See how this works? In the sample scripts found in the Script Center, the first line of code is often this:
strComputer = "."
That code assigns a dot (.) to the variable strComputer; we do that with WMI scripts because—in WMI lingo—a dot represents the local computer. That means, by default, most of our scripts run against the local computer.
With our modified script, all we’ve done is make one little change: instead of assigning the value dot to the variable strComputer, we assign the value of the first command-line argument we typed in. Want to run the script against the computer redmond-ws-87? Then start the script by typing this:
cscript args.vbs redmond-ws-87
Want to run it against the local computer? Just use a dot as your command-line argument:
cscript args.vbs .
If this wasn’t worth a 17-year wait, we don’t know what is.
That’s cool, but it would be even cooler if we could….
Yes, you’re right. We now have a script that will stop the Alerter service on any computer in the universe (assuming, of course, we have local administrator rights on that computer). That’s OK, but we’re willing to bet that system administrators don’t spend all that much time stopping the Alerter service on their computers. What would really be nice is a script that could stop any service on any computer. But is such a thing even possible?
Well, it took us 17 years, but we managed to find the answer: yes, it is possible. Before we show you how to do that, though, let’s take a pop quiz. What do you think WSH does with the arguments when we start a script using this command:
cscript args.vbs atl-ws-01 cisvc
That’s right; just like any other script, the two command-line arguments are automatically put into the Arguments collection. That means the Arguments collection for this script looks like this:
Index No. |
Value |
---|---|
0 |
atl-ws-01 |
1 |
cisvc |
Impressive, huh? But wait, we’re not done yet. Just a few minutes ago we showed you how to access the first command-line argument using code similar to this:
strComputer = Wscript.Arguments.Item(0)
Wscript.Echo strComputer
Nothing too fancy there: we just assign the value of the first argument (index number 0) to the variable strComputer. So how do you suppose we make use of the second command-line argument (index number 1)? That’s right; we just assign that value to a second variable:
strComputer = Wscript.Arguments.Item(0)
Wscript.Echo strComputer
strService = Wscript.Arguments.Item(1)
Wscript.Echo strService
When you run this script using the command cscript args.vbs atl-ws-01 cisvc you get back output like this:
atl-ws-01
cisvc
Are you detecting a pattern here? Suppose we passed a script 199 command-line arguments; how would you access the last argument? You’d use code like this:
strLastArgument = Wscript.Arguments.Item(198)
Wscript.Echo strLastArgument
Why Wscript.Arguments.Item(198)? Remember, the first item in the Arguments array is index number 0; the second item is index number 1. If you extrapolate from there, you’ll see that the 199th item in the array has an index number of 198. Thus Wscript.Arguments.Item(198). What if we wanted to get at the 37th argument in the list. You got it: Wscript.Arguments.Item(36).
From here it’s just a hop, skip, and a jump to creating a script that can stop any service on any computer. All we have to do is:
Include code that assigns the name of the service to the variable strService.
Reference strService in our WHERE clause.
The finished product might look like this:
strComputer = Wscript.Arguments.Item(0)
strService = Wscript.Arguments.Item(1)
Set objWMIService = GetObject _
("winmgmts:\\" & strComputer & "\root\cimv2")
Set colServices = objWMIService.ExecQuery _
("Select * FROM Win32_Service WHERE Name = '" & strService & "'")
For Each objService in colServices
objService.StopService
Next
Note Yes, the WHERE clause looks a little weird now, with single quote marks and double quote marks and ampersands and all sorts of crazy punctuation. If you’re not sure how this “string concatenation” works, take a look at this excerpt from the Microsoft Windows 2000 Scripting Guide. |
What if we wanted to use three arguments? For example, suppose we wanted to give people the option of starting a service as well as stopping it. Well, we could start the script using a command similar to this:
cscript args.vbs atl-ws-01 cisvc start
And our script to make use of three arguments might look like this:
strComputer = Wscript.Arguments.Item(0)
strService = Wscript.Arguments.Item(1)
strAction = Wscript.Arguments.Item(2)
Set objWMIService = GetObject _
("winmgmts:\\" & strComputer & "\root\cimv2")
Set colServices = objWMIService.ExecQuery _
("Select * FROM Win32_Service WHERE Name = '" & strService & "'")
For Each objService in colServices
If strAction = "start" Then
objService.StartService
ElseIf strAction = "stop" Then
objService.StopService
End If
Next
Notice that the action to take (start) is the third argument passed to the script, so we assign Wscript.Arguments.Item(2) to the variable strAction.
By the way, this script works great as long as you supply the arguments in the correct order. For example, see what happens if you start the script using this command:
cscript args.vbs start atl-ws-01 cisvc
Yep: the script will try to connect to a computer named start, and everything will go downhill from there. Is there a way to work around this problem? As a matter of fact there is, and we’ll show you how to do that momentarily.
One is the loneliest number
No doubt you’re glad that you can pass a single computer name to the script, but now you’re probably wondering if you could pass multiple computer names to that script. In other words, you’d like to type something like this:
cscript args.vbs atl-ws-01 atl-ws-02 atl-ws-03 atl-ws-04
And then you’d like the script to run against these four computers:
atl-ws-01
atl-ws-02
atl-ws-03
atl-ws-04
Is that possible? You bet it is, although maybe not in the way you’re thinking. You’re thinking, “OK, atl-ws-01 is the first item in the array, so I could get to it by calling Wscript.Arguments.Item(0), and atl-ws-02 is the second item in the array, so I could get to it by calling Wscript.Arguments.Item(1), and ….” If that’s what you’re thinking, then stop: that won’t work.
Well, OK, technically, it could work, but coding it would be a nightmare. What if someone typed in only three computer names? In that case, and without proper error-handling, the script would blow up when you tried to access Wscript.Arguments.Item(3), the non-existent fourth item in the list. What if a user typed in five computer names, or 50 computer names, or 500? Trust us: trying to reference the arguments by their individual index numbers would be a real nightmare.
Fortunately, there’s a better way to do this. Remember how we mentioned that arguments are kept in the Arguments collection? Big deal, you say? Well, actually, it is a big deal; if you recall your first day of Scripting 101, then you know that you can access all the arguments in a collection by using a For Each loop. Type this code into Notepad and save it as args.vbs:
For Each strArgument in Wscript.Arguments
Wscript.Echo strArgument
Next
Now run the script using this command:
cscript args.vbs atl-ws-01 atl-ws-02 atl-ws-03 atl-ws-04
And what do you get back? That’s right, the script echoes back each of your command-line arguments, even though you never referenced a single index number:
atl-ws-01
atl-ws-02
atl-ws-03
atl-ws-04
Now we’re on to something. Suppose you want a script that can stop the Alerter service on any number of computers. All you had to do was ask:
For Each strArgument in Wscript.Arguments
strComputer = strArgument
Set objWMIService = GetObject _
("winmgmts:\\" & strComputer & "\root\cimv2")
Set colServices = objWMIService.ExecQuery _
("Select * FROM Win32_Service WHERE Name = 'Alerter'")
For Each objService in colServices
objService.StopService
Next
Next
As you can see, this is pretty simple. We create a For Each loop that loops through the Arguments collection. The very first line of code within that loop assigns the value of the current argument to the variable strComputer, which is then used to represent the first computer we connect to. When we run the script, the script takes the first argument—atl-ws-01—and assigns it to the variable strComputer. The script then connects to that computer, stops the Alerter service, and loops around to see if there are any other arguments in the collection.
As it turns out, we do have some more arguments. Consequently, the script grabs the second argument—atl-ws-02—and assigns that value to the variable strComputer. This continues until the script runs out of arguments. At that point, it simply exits the For Each loop and, in this case, ends.
Very simple, and makes it very easy to adapt almost any script to run against any number of computers.
What about any service on any number of computers?
What’s that? We were afraid that someone was going to ask that. The reader in the blue hat wants to know if we can create a script that stops any service on any number of computers. Can we do that? Hey, we’re the Scripting Guys; we can do anything. (Well, as long as we’re given 17 years in which to do it.) This kind of script is a touch more complicated, but let’s give it a whirl.
To begin with, we have a confession to make: we’ve been holding out on you. We told you that WSH has an Arguments collection, and that’s true; it does. (Hey, you can look it up.) However, WSH also has two other Argument collections we didn’t mention: Named and Unnamed. If you want to do something crazy, like stop any service on any number of computers, the easiest way to do that is to make use of these hitherto unmentioned collections.
So what are named and unnamed arguments? Well, consider this simple command-line string:
cscript args.vbs /service:alerter atl-ws-01
Notice that we have two arguments here: /service:alerter and atl-ws-01. If you run a script to see what’s in the Arguments collection, you’ll get back two items: /service:alerter and atl-ws-01.
But notice the format of the first argument: /service:alerter. Why did we pass the argument like this? Well, WSH is designed to use something called a named argument. With a named argument, arguments take the format /argument_name:argument_value. In other words, in this example, we have an argument named service, and the value of that argument is alerter.
Should we care about that? You bet we should. Take a look at this code:
Wscript.Echo Wscript.Arguments.Named("service")
What’s going on here? Well, we’re echoing back the value of the argument named service. When we do so, we get back the value alerter. See how that works? Suppose we started the script using this command:
cscript args.vbs /service:cisvc atl-ws-01
What would get back? That’s right: cisvc. That’s because, with this command string, the value of the argument named service is cisvc.
Confused? Don’t be; this is really is pretty easy. Suppose we start a script using this command:
cscript args.vbs /service:cisvc /computer:atl-ws-01
Our named arguments collection would look like this:
Name |
Value |
---|---|
service |
cisvc |
computer |
atl-ws01 |
And we would reference those arguments in our script by using code like this:
Wscript.Echo Wscript.Arguments.Named("service")
Wscript.Echo Wscript.Arguments.Named("computer")
Play around with this a little, and you’ll see that it actually does make sense.
But, uh, what about any service on any number of computers?
Be patient. You want to know how named and unnamed arguments helps us with this command:
cscript args.vbs /service:alerter atl-ws-01
And before we go any further, you’re right, it’s easy to get at the service argument using code like this:
Wscript.Echo Wscript.Arguments.Named("service")
But how do we get at the computer name tacked on the end?
Much easier than you might think. To get at the computer name we simply reference the WSH Unnamed Arguments collection. (You’ll notice that atl-ws-01 is an argument without a name.) Could we have given this argument a name? Sure, we could have done this:
cscript args.vbs /service:alerter /computer:atl-ws-01
That would make it easy to stop any one service on any one computer. But remember, we wanted to stop a specified service on a bunch of computers. What we really want is the ability to type in a command like this:
cscript args.vbs /service:alerter atl-ws-01 atl-ws-02 atl-ws-03 atl-ws-04
But then how do we reference all four of those computers? Well, the easiest way is to use the WSH Unnamed Arguments collections. For example, type in this code and save it as args.vbs:
For Each objArgument in Wscript.Arguments.Unnamed
Wscript.Echo objArgument
Next
Now run the script using this command:
cscript args.vbs /service:alerter atl-ws-01 atl-ws-02 atl-ws-03 atl-ws-04
What happens? You got it: only the four computer names—the four unnamed arguments —are echoed back. Why isn’t the service argument echoed? Simple: it’s a named argument, which means it isn’t part of the Unnamed arguments collection. (If you looped through the Wscript.Arguments.Named collection, service would show up.)
Just to make sure everyone understands what’s going on, let’s take a peek at the various Argument collections. Here’s what the Arguments collection (which consists of all arguments, both named and unnamed) contains:
Arguments Collection |
---|
/service:alerter |
atl-ws-01 |
atl-ws-02 |
atl-ws-03 |
atl-ws-04 |
Got that? OK, then here’s what the Named Arguments collection looks like:
Named Arguments Collection |
Value |
---|---|
service |
alerter |
Note that a named argument consists of two parts: the name and the value.
And, finally, here’s what we have in the Unnamed Arguments collection:
Unnamed Arguments Collection |
---|
atl-ws-01 |
atl-ws-02 |
atl-ws-03 |
atl-ws-04 |
Believe it or not, we’re now home free. Type this script into Notepad and save it as args.vbs:
Wscript.Echo Wscript.Arguments.Named("service")
For Each objArgument in Wscript.Arguments.Unnamed
Wscript.Echo objArgument
Next
Now run the script using this command:
cscript args.vbs /service:alerter atl-ws-01 atl-ws-02 atl-ws-03 atl-ws-04
Surprise! The script first echoes the value of the named argument service, and then echoes the values of the four unnamed arguments. We now have an answer to the question, “Can I write a script that stops any service on any number of computers?” Of course you can:
strService = Wscript.Arguments.Named("service")For Each strArgument in Wscript.Arguments.Unnamed strComputer = strArgument Set objWMIService = GetObject _ ("winmgmts:\\" & strComputer & "\root\cimv2") Set colServices = objWMIService.ExecQuery _ ("Select * FROM Win32_Service WHERE " & _ "Name = '" & strService & "'") For Each objService in colServices objService.StopService NextNext
Again, pretty simple stuff. First we grab the value of the argument named service, and store that value in the variable strService. We then create a For Each loop that loops through all the unnamed arguments, which in this case happens to be the computers we want to run the script against. Each time we cycle through the loop, we grab a computer name and assign it to the variable strComputer. Each time through the loop we also run the main body of the script which:
Connects to the computer specified by strComputer.
Stops the service specified by strService.
It’s a thing of beauty, isn’t it?
In case you’re wondering, named arguments are also the way to work around the problem of users passing arguments in different orders. For example, suppose a user starts a script using this command:
cscript args.vbs /service:alerter /computer:atl-ws-01
Is that any different from a second user who starts the same script using this command:
cscript args.vbs /computer:atl-ws-01 /service:alerter
Nope, no difference whatsoever. Why? Remember, when dealing with named arguments we use the name, not the index number. In this case we’re looking for an argument named computer and another one named service; we don’t care which order they were passed. With named arguments, the index number is irrelevant. That’s not always true for unnamed arguments.
Where do we go from here?
So is this everything you can do with command-line arguments? No, not even close; for example, there are ways to test whether a particular command-line argument exists and, if it doesn’t, to prompt the user to enter it. (After all, what good is a script that stops a service if the user fails to specify the service he or she wants to stop?) We don’t have time to go into these other features today, but you might want to check out this portion of the Microsoft Windows 2000 Scripting Guide, which provides additional information on command-line arguments and how to best use them in a script.
In the meantime, we’ll get to work on next month’s column. After all, the year 2021 will be here before you know it.