Bring in da Subs, Bring in da Funcs - Building scripts with procedures

By The Microsoft Scripting Guys

Doctor Scripto at work

Doctor Scripto's Script Shop welds simple scripting examples together into more complex scripts to solve practical system administration scripting problems, often from the experiences of our readers. His contraptions aren't comprehensive or bullet-proof. But they do show how to build effective scripts from reusable code modules, handle errors and return codes, get input and output from different sources, run against multiple machines, and do other things you might want to do in your production scripts.

We hope find these columns and scripts useful – please let us know what you think of them. We'd also like to hear about other solutions to these problems that you've come up with and topics you'd like to see covered here in the future.

For an archive of previous columns, see the Doctor Scripto's Script Shop archive.

On This Page

Bring in da Subs, Bring in da Funcs - Building Scripts with Procedures
Modularity and Family Values
Writing Procedures
Subroutines
Calling subroutines
Functions
Calling functions
Modeling Blackjack with Procedures
Scoping It Out
Avoiding variable confusion
Objects and Procedures
That's Not Real Code
Deciding When to Use Procedures in Scripts
Where Do I Go to Learn More About Procedure?

Bring in da Subs, Bring in da Funcs - Building Scripts with Procedures

In Doctor Scripto's Script Shop columns so far, many of the longer scripts have been divided into subroutines or functions. "Procedures" is the generic name for these building blocks. You may have noticed that this is not the case in most of the rest of the TechNet Script Center. That's because Greg claims that use of procedures causes premature senility and excessive nose hair growth. What? Greg says he never made any such claims. He just doesn't like procedures: read why in "Greg Smacks Down Procedures ," a Script Center exclusive.

In spite of these concerns, there are also potential rewards in using procedures to make scripts modular. These benefits increase in proportion to the size and complexity of the script. As you've seen, Doctor Scripto loves gnarliness and intricacy, so procedures are his cup of tea. If you do decide to use procedures in your scripts, though, you might want to hedge your bets by consulting your psychiatrist and cosmetologist.

Most of the scripts in the Script Repository are simple ones of a few lines that perform one task; for these, breaking out the code into modules wouldn't make a lot of sense. When you start to put several of these simple scripts together, though, chunking the result into functions or subroutines that each perform a distinct job can provide clearer ways to handle the relationships, execution paths and data flows. In addition, well-thought-out procedures can make scripts more understandable for other scripters who may have to maintain them. Haven't yet met a script that uses procedures? See any of the Script Shop columns; one substantial example is Listing 7 of "Controlling pest-ware with asynchronous event monitoring."

As Shakespeare might have written if he wrote our kind of scripts, procedures give to airy nothingness a name and parameters. When you call a function named ReadTextFile, the name gives you a pretty fair idea of what the module ought to do. If they're reasonably named, the parameters (what you pass to the function) and return value (what you get back from the function) should also communicate what kind of information the function is receiving and sending. For instance:

arrLines = ReadTextFile(strInputFile)

might lead you to believe that you were passing the name of the file to the function and, in return, receiving an array of lines of text. And, given those names, that's what it better do.

This column, we hope, will help you get started with procedures should you decide to try them. It's a bit of a departure from Dr. Scripto’s previous columns in that it goes back to the basics, but it's healthy for him to come down out of the clouds of details once in a while and dig into first principles of scripting.

Modularity and Family Values

Doctor Scripto's enthusiasm aside, why should you invest your scarce time in learning a different approach to constructing scripts? Well, for one thing, building up a library of procedures can save script development time and encourage more automation with scripts. Although scripting can ultimately save you time and aggravation as a system administrator, first you have to find the time to write the scripts. Repurposing and refining existing code modules, procedural or non-procedural, can speed script development and help you get over this productivity hump. Working this way also encourages boiling down important modules to optimized and tested script code that you can rely on.

This kind of library can encourage less-experienced scripters to learn from and build on the shared code. If you have more than one scripter in your shop, you can break large projects down into pieces to be developed separately by different scripters and then reassembled. These sorts of well-organized common code modules can help bring your IT family together. And the Scripting Guys are very big on family values.

Modularity can also mean more flexibility in experimenting with different ways to solve a scripting problem: it's often easier to try out a potentially useful function by plugging it in and calling it than to try to fit a chunk of code into a script that's one long procedure. Finally, modularity can make debugging easier. When code is broken into sections, you can test one section at a time or comment out a problematic one to see the effect on the script as a whole. In a long script, that can save you troubleshooting time and aggravation.

Beyond these benefits, using or not using procedures is a matter of style. If you're working alone, style comes down to personal preference. If you work in an IT group where more than one person writes scripts, though, you may need to resolve the question of how to structure scripts with group scripting standards.

Writing Procedures

If you're like the Scripting Guys, at this point you're probably chomping at the bit and muttering "Cut to the chase, show us some code." Well, your time has come: next we're going to talk about the nuts and bolts of writing subs and functions.

If you've been reading Doctor Scripto’s Script Shop regularly, you've already seen enough subroutines and functions to get a practical idea of how they can be put together. But we've never gotten down to the nitty-gritty and talked about what they are and the basics of how they work. If you're an experienced scripter, bear with us here and we'll eventually get to some issues that may be of interest to you too.

OK, now we're going to show you the code:

strInputFile = "list.txt"

WScript.Echo "Text file contents:"
For Each strItem In ReadTextFile(strInputFile)
  WScript.Echo strItem
Next

'******************************************************************************

Function ReadTextFile(strInputFile)
'Read contents of text file and return array with one element for each line.

On Error Resume Next

Const FOR_READING = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
If Not objFSO.FileExists(strInputFile) Then
  WScript.Echo "Input text file " & strInputFile & " not found."
  WScript.Quit(1)
End If
Set objTextStream = objFSO.OpenTextFile(strInputFile, FOR_READING)
If objTextStream.AtEndOfStream Then
  WScript.Echo "Input text file " & strInputFile & " is empty."
  WScript.Quit(2)
End If
arrLines = Split(objTextStream.ReadAll, vbCrLf)
objTextStream.Close

ReadTextFile = arrLines

End Function

That was the code. What, you want an explanation too? Sorry, that wasn't included in the introductory price. By now you've probably figured out what ReadTextFile does anyway. You may even have seen it in previous Script Shop columns. All right, all right, we'll come back and talk about this function later, but first let's figure out some of the ground rules of subroutines and functions. For now, let's just say that if you need to read a text file more than once in your script, putting this code into a procedure will slim down your code.

Subroutines

Because subroutines are simpler than functions, we're going to start with them. To create a subroutine, you use the Sub statement followed by the subroutine's name. You also have to demarcate the end of the subroutine. So the framework looks like this:

Sub DoSomething
  'Do something here.
End Sub

The internal code of the sub must be sandwiched between the Sub and End Sub lines.

The “something” the sub does can be as simple as displaying a message:

Sub SaySomething
  WScript.Echo "Coming to you live from inside the Sub."
End Sub

You can name the sub anything you want within the limits of your fertile imagination and VBScript variable-naming rules, but most organizations follow some kind of naming convention. The Scripting Guys usually use a verb-noun format, as in ReadTextFile. And we capitalize each word (this is known as Pascal casing, after the programming language). This is the convention used by WMI for most of its methods, and if it's good enough for WMI it's good enough for us. When reading the script, this convention also helps you distinguish procedure names from variable names. For variable names we commonly use the convention of camel casing (first word lower-case, the rest initial capitalized). We also use Hungarian notation, which begins the name with three letters indicating the type of data in the variable: for example, strFileName for a string.

Procedures crave communication: you can actually have a conversation with them. Subroutines are the strong, silent type: they simply do something with the information you give them internally – although this can include displaying that information with WScript.Echo or calling another sub or function to do something with the transformed information (say, write it to a text file). Functions, on the other hand, are the loose lips that sink ships: as we'll see, they can also pass information back out.

If you want to give a subroutine or function some external data to work with, you have to add something to the basic framework we just set up: you have to tell the procedure what parameters will be passed to it. You enclose parameters in parentheses and add them after the name of the procedure.

Sub DoSomethingWith(strParameter1)
  'Do something with strParameter1 here.
End Sub

Accepting parameters can make a sub more versatile. If you add a parameter to the SaySomething sub, it can echo a different message depending on where in the script body it's called.

Sub SaySomething(strMessage)
  WScript.Echo strMessage
End Sub

Whatever the calling statement passes in, SaySomething will display. Different parts of the script can pass different things to display, and SaySomething will faithfully communicate each one to the outside world. This is an improvement on a hard-coded message, although you're still using a three-line sub to do what we could accomplish with one line in the body. So don't go using this example code in your working scripts – take it as an illustration of the value of parameters.

Now you've got a working sub-routine, if not a very practical one. But if you put it in a script as is, it will just sit there radiating Sub-ness: it still doesn't do anything by itself.

Calling subroutines

So how do you use this subroutine? To take advantage of the sublimity of SaySomething, you have to call it from somewhere else in your script. This can be from the body of the script or from another subroutine or function. You can call it as many times as you like. And if the sub requires parameters, you have to pass them into the sub with your call.

To call a sub that doesn't take any parameters, simply type the sub's name. The following is a valid call to a sub: run it and it displays the message from the sub on the command line.

SaySomething

Sub SaySomething
  WScript.Echo "Coming to you live from inside the Sub."
End Sub

To call a sub that takes a parameter, you type the name of the sub followed by a space and the parameter to pass. The following calls the sub SaySomething and passes it the string "Something." as a parameter.

SaySomething "Something."

Sub SaySomething(strMessage)
  WScript.Echo strMessage
End Sub

Parameters are like the arguments you type on the command line after the name of a command-line tool. When you type in an argument after the tool, you're passing in a piece of information to the tool that says to it: do something with this.

When you type:

nslookup microsoft.com

the argument "microsoft.com" means: tell me all about this domain, show me the full Monty on the microsoft.com name servers.

The Scripting Guys love a good argument, as Greg can testify. But when we work with procedures we call them parameters. The syntax is different but the idea is the same: give the procedure (or command-line tool) something to chew on.

Try putting this into a text file, naming it with a .vbs extension – how about saysomething.vbs? – and running cscript saysomething.vbs at the command prompt.

SaySomething "Coming to you live from outside the Sub."
SaySomething "Brought to you by SaySomething."

Sub SaySomething(strMessage)
  WScript.Echo strMessage
End Sub

You should see both messages in living black-and-white on your monitor.

But the excitement doesn't stop there. You must pass as many parameters into the sub as the sub is set up to accept. If the sub takes more than one parameter, you separate the parameters in the call with a comma.

SaySomethingIf "Coming to you live from outside the Sub.", True
SaySomethingIf "Brought to you by SaySomething.", False

Sub SaySomethingIf(strMessage, blnIf)
  If blnIf Then
    WScript.Echo strMessage
  Else
    WScript.Echo "The message could not be displayed. Please try again later."
  End If
End Sub

This devious sub takes a second parameter that's a Boolean variable: its value is either True or False. It's like a switch that turns the passed-in message on or off. The If … Then … Else statement in the sub checks whether the value of the variable is True: if it is, the sub displays the passed message; if it's False, it displays an annoying error message. As this script illustrates, Doctor Scripto learned the craft of error message writing from a cantankerous voice-mail machine that didn't like to answer the phone. When you run this script, it displays the first message passed in followed by the error message.

Functions

Declaring a function is not very different from declaring a subroutine. You just substitute Function for Sub in the opening and closing statements:

Function DoSomething
  'Do something here.
End Function

You tell a function the parameters to expect exactly as you would a sub:

Function DoSomethingWith(strParameter1)
  'Do something with strParameter1 here.
End Function

What distinguishes a function from a sub in VBScript is that you can also get information back out of a function that the script can then use. In other words, functions can return a value. They often return this value explicitly. But if they don't, VBScript does it for them implicitly. A function in which no return value is assigned returns either 0, an empty string (""), or the Nothing keyword, depending on whether VBScript expects a number, string or object reference back.

Of course, you don't have to do anything with the return value: you can leave it to languish unused in the function call. This means that you can use a function just like a sub if you want. In fact, many languages outside of the BASIC family, such as C++ and JScript, don't have subs, only functions. If you don't want to return a value when you're coding in the C family, for example, you can declare a return type of void. But since the verbose BASIC clan lets us make the distinction, we might as well make the best of it.

To return a value from a function, you assign that value to the name of the function. The value returned can contain any type of data: string, number, Boolean, array, what have you. The function name is treated by VBScript as a variable, and that variable carries the output of the function back to the calling statement as the return value, as we'll see a little later.

Function AddMe(intNumber)
  AddMe = intNumber + 2
End Function

A function can return values contingent on decisions made by the function code. You can assign different return values as many times as you need to within a function. Let's say you don't like the number 6 – maybe you lost a lot of money playing blackjack in Las Vegas when you needed a 5 but you got a 6, and you don't want to be reminded of it. You can code the script to avoid those bad feelings.

Function AddMe(intNumber)
  If intNumber >= 6 Then
    AddMe = 0
  Else
    AddMe = intNumber + 2
  End If
End Function

If the value passed in is greater than or equal to 6 (because, given the rules of blackjack, you didn’t want a number greater than 6 either), AddMe returns 0; otherwise it just adds 2 as before and returns that value. So you return different values depending on decisions made within the function. See how much power procedures give you? Unfortunately, the Scripting Guys haven't yet found a way to use procedures to win at blackjack.

So you can visualize functions as machines on an assembly line that take inputs and transform them into outputs. But instead of stamping steel bars into brackets, functions accept strings or numbers or arrays or objects and return other data, not necessarily of the same type.

Calling functions

When you call a function, you enclose the parameters in parentheses, unlike the naked parameters in a sub call.

WScript.Echo AddMe(2)

Function AddMe(intNumber)
  AddMe = intNumber + 2
End Function

Multiple parameters are separated by commas, as with subs.

WScript.Echo AddUs(2, 3)

Function AddUs (intFirstNumber, intSecondNumber)
  AddUs = intFirstNumber + intSecondNumber
End Function

Another difference between calls to subs and calls to functions is that you can put the function call on the right side of an assignment to another variable:

intSum = AddUs(2, 3)

Because VBScript treats a function call as a variable, you can think of the call as a memory location where the function writes its result. You can grab the data out of that memory location just as you can with any other variable. You can also perform operations with it:

intSum = AddUs(2, 3) * 4

And you can make decisions based on the value returned. Let's try this with our blackjack script. Let's simulate a dealer with the VBScript randomization functions.

Randomize
intDeal = Int((Rnd * 10) + 1)
WScript.Echo intDeal

If AddMe(intDeal) > 0 Then
  WScript.Echo "Hit"
Else
  WScript.Echo "Stand"
End If

Function AddMe(intNumber)
  If intNumber >= 6 Then
    AddMe = 0
  Else
    AddMe = intNumber + 2
  End If
End Function

Well, all right, that wasn't a very good simulation of blackjack. But wouldn't it be nice if you could see the card before you had to decide whether to take it or not?

If the random number intDeal is greater than or equal to 6, AddMe returns 0 and the script stands (doesn't ask for another card from the dealer). If the number is less than 6, the script calls for a hit (ask for another card). But AddMe always adds 2, which is pretty much the way the house works. They've got to make money, don't they? With these kinds of sophisticated algorithms, you'd be better off playing the nickel slots. (NOTE: Stick with us a second while we make the lawyers happy: Microsoft and the Scripting Guys do not advocate gambling of any kind. We do not approve or disapprove of gambling. As a matter of fact, we don’t actually care whether you gamble or not, as long as it’s not our money.)

While we're on the subject of dubious functionality, VBScript does have a Call statement that you can use before the name of your procedure, but we haven't found any good reason to use it. It just complicates things if you're calling a sub: if you use Call, you have to enclose the parameters in parentheses, the reverse of calling a sub without Call. Call doesn't change anything for functions: you have to use parentheses either way. So unless someone can come up with a compelling argument in its favor, Call will stay on our list of harmless scripting eccentricities (along with Peter).

Modeling Blackjack with Procedures

If we were actually trying to model a blackjack game in a script, we might want to break things down into more granular pieces. For example, we could pull out the randomizing code into a separate function called DealCard that takes no parameters but returns a random number from 1 to 11 (we won't worry about how to deal with aces, which can be 1 or 11). This would enable us to easily reuse the functionality of dealing a card.

To create a hand, we could create a separate function, DealHand, that calls DealCard two times and returns the sum of the cards. Then we might add a decision-making function, Decide, that looks at that sum and decides whether or not to take a hit. This is where the player would have to choose, so we could ask for user input at this point, but alternatively we could build the probability calculations into this function and let it play for us. If it decided to take a hit, it would call HitMe, which would call DealCard once.

We'd probably also need another function, CountHand, which kept track of the sum of the cards after the deal. DealHand and HitMe would both pass their return values to CountHand. And we haven't even modeled the dealer's hand yet, or dealt with the higher probability of getting 10, the value of all three face cards.

We could go on making the simulation more complex and realistic, but you get the idea. When we start to model a process in code, a powerful tool we can use is to break it down into identifiable chunks that each perform a logical piece of the process.

There's a concept in object-oriented programming called encapsulation that comes close to what we're talking about here. Encapsulation generally means the enclosing of one thing in another; in programming, it refers more specifically to bundling together information and the operations performed on that information into a separate entity.

In object-oriented programming using a system-level language such as C++, developers tend to break down functionality into molecular chunks. In scripts, in contrast, our goal is rapid development and we don't usually have to fit our pieces into a gigantic jigsaw puzzle to the extent that application and operating system developers usually do.

Still, encapsulating script code in procedures, based on the criteria we talked about earlier, can help our script development process. Scripters should just practice it in moderation. And be sure to get regular checkups.

Scoping It Out

Once we move into the territory of procedures, our variables run into the question of borders and how to cross them. The areas of a script where a variable's passport is valid – that is, where the variable is recognized – are called the variable’s "scope."

In an undivided script with no procedures, there can be only one variable or constant with a given name for that script, and the scope of all variables is the whole script. But once you introduce a procedure, you have to be aware of the scope of the variables used inside and outside of it.

VBScript is different from most system-level programming languages in that it doesn't require you to declare variables before you use them. Because that saves time and lines of script, you may have noticed that in most of the scripts in the Script Center we don't officially declare variables like this:

Dim strComputer

We just initialize them, which has the effect of declaring them as well.

strComputer = "."

If you declare or initialize a variable in the main body of the script, the scope of that variable is the whole script. For example, if the first line of your script is:

strFileName = "hosts.txt"

the variable strFileName will be recognized throughout the script, including in any procedures. Not only can you read the variable anywhere, but you can also change the value in any part of the script. So if inside a function you assign a new value to strFileName:

strFileName = "targets.txt"

the value of strFileName is now changed for the entire script.

To see how this works, try running this script:

strFileName = "hosts.txt"
WScript.Echo strFileName
ChangeName
WScript.Echo strFileName

Sub ChangeName
  strFileName = "targets.txt"
End Sub

Output:

hosts.txt
targets.txt

Variables initialized in the main body of the script, which are valid throughout the script, are called global variables.

If you initialize a new variable inside a procedure, though, the picture changes. That variable is a local variable. It's recognized only within that procedure, so we say it has local scope. For example, if you use a counter variable in a For loop within a sub, that counter retains the value assigned to it only inside the sub. The variable is destroyed when the sub ends, and the memory it used is released.

Counter
WScript.Echo i + 5

Sub Counter
For i = 1 To 10
  WScript.Echo i
Next
End Sub

Output:

1
2
3
4
5
6
7
8
9
10
5

When we try to do something with the variable i outside of the sub, the script doesn't know that the variable existed inside the sub, because that i ceased to exist when the sub exited. So the final value of 10 that was assigned to i inside the sub is gone with the wind.

When VBScript sees a new variable, i, in the script body that hasn't been initialized but that is used as if it were a number, the interpreter very helpfully treats it as a number and initializes it to 0. It then adds it to 5, which is how we get the final output value. To belabor our Las Vegas theme, what happens in the procedure stays in the procedure – at least in this case.

Avoiding variable confusion

VBScript is a very forgiving and helpful language, which ironically can make dealing with scope trickier in some cases than with more rigid languages. If you have already used a variable in the main body of the script, but forget and use a variable of the same name inside a procedure, you can get confusing results. You've inadvertently created a global variable, but in the procedure you're not treating it as a global variable.

A simple way to avoid this problem is to name global variables with a different prefix from local variables. In some of our scripts we use "g_" before the variable names of global variables. So a variable with global scope containing an IP address might be named g_strIPAddress.

VBScript also provides more formal mechanisms to avoid colliding variables. We rarely use them in our scripts, but when you're dealing with complex scripts being developed by more than one person they may come in handy.

If you begin a script with the statement Option Explicit, then all variables in that script must be declared before they are initialized or used. If you use a variable without declaring it, you get an error.

Option Explicit

Dim strComputer
Dim strFileName
Dim intCounter

strIPAddress = "192.168.0.1"

If you run this code, you'll get an error on the final line because you haven't declared strIPAddress before using it:

Microsoft VBScript runtime error: Variable is undefined: 'strIPAddress'

If you use Option Explicit and declare variables in the script body and in procedures, you make it much harder to accidentally declare variables of the same name in the script body and in a procedure. This helps to eliminate the possibility of variable collisions. So you can save a bundle on your variable insurance. (If this makes no sense, try watching sports on U.S. television for a few hours.)

Another way to keep variable scope straight is to use ByVal (by value) and ByRef (by reference) before the variable name when defining it as a parameter of a procedure. ByVal and ByRef help make explicit how the variable will be handled in the procedure.

When you pass a variable by reference, you're passing a reference or pointer to the memory location that holds the value of the variable. If the procedure changes the value of that variable, the change occurs in the original memory location. So the value changes for the whole script. Passing a variable ByRef is equivalent to treating it as global: it would have the same effect if you simply used the global variable in the procedure without passing it in. If you don't indicate ByRef or ByVal for a parameter, the parameter defaults to ByRef.

By contrast, when you pass a variable to a procedure by value, you're copying the value of that variable to another variable local to that procedure, even if it has the same name as the global variable. The procedure can then change the value of the variable internally, but the change does not affect the value of the variable in the body of the script or in other procedures. In effect, you've created two independent variables, one with local scope within the procedure and the other whose scope is limited to the body of the script (and any other procedures to which it may be passed by reference).

strComputer = "localhost"
strFileName = "hosts.txt"
WScript.Echo "Before sub call: " & strComputer & " " & strFileName
ChangeUs strComputer, strFileName
WScript.Echo "After sub call: " & strComputer & " " & strFileName

Sub ChangeUs(ByVal strComputer, ByRef strFileName)

  strComputer = "sea-wks-3"
  strFileName = "computers.txt"
  WScript.Echo "Inside sub: " & strComputer & " " & strFileName

End Sub

Output:

Before sub call: localhost hosts.txt
Inside sub: sea-wks-3 computers.txt
After sub call: localhost computers.txt

If you don't specify ByVal or ByRef, the parameters are implicitly passed ByRef. This is true even if the names of the variables listed as sub parameters are different from those in the script body. All that matters is the order of the parameters: the first parameter listed after the sub name references the same memory location as the first parameter in the call to the sub, the second parameter in the sub maps to the second in the call, and so forth. If you change the value of the parameter in the sub, the value of the parameter in the script body is changed as well.

strComputer = "localhost"
strFileName = "hosts.txt"
WScript.Echo "Before sub call: " & strComputer & " " & strFileName
ChangeMe strComputer, strFileName
WScript.Echo "After sub call: " & strComputer & " " & strFileName

Sub ChangeMe(strComp, strFile)

  strComp = "sea-wks-3"
  strFile = "computers.txt"
  WScript.Echo "Inside sub: " & " " & strComp & " " & strFile

End Sub

Output:

Before sub call: localhost hosts.txt
Inside sub: sea-wks-3 computers.txt
After sub call: sea-wks-3 computers.txt

Objects and Procedures

When the variable you're dealing with is an object reference, the same rules of scoping apply as they do to variables of other types. You can pass objects as parameters into procedures. You can also get objects back from a function as a return value. In both cases you need to keep track of the scope of the object references, which also entails making sure that each new object reference within a scope is assigned with the Set statement.

When you pass an object reference as a parameter, it already has an object or collection assigned to it with Set. Within a function, when an object reference is used as a return value, the function must also assign the object reference to the function name with Set, and the variable to which the return value is assigned by the function must also be an object reference initialized with Set.

Doctor Scripto looks dizzy after trying to get his head around this: obviously it's easier to show in an example than to explain abstractly. The following script passes standard WMI objects and collections between the script body and two functions:

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")

Set colProcessors = GetProcessors(objWMIService)
WScript.Echo vbCrLf & "PROCESSORS"
For Each objProcessor In colProcessors
  WScript.Echo vbCrLf & "Name: " & objProcessor.Name
  WScript.Echo "Description: " & objProcessor.Description
  WScript.Echo "Maximumum Clock Speed: " & objProcessor.MaxClockSpeed
  WScript.Echo "Address Width: " & objProcessor.AddressWidth
Next

Set colPrinters = GetPrinters(objWMIService)
WScript.Echo vbCrLf & "PRINTERS"
For Each objPrinter In colPrinters
  WScript.Echo vbCrLf & "Device ID: " & objPrinter.DeviceID
  WScript.Echo "PortName: " & objPrinter.PortName
Next

'******************************************************************************

Function GetProcessors(objWMIService)

Set colProcs = objWMIService.ExecQuery("SELECT * FROM Win32_Processor")
Set GetProcessors = colProcs

End Function

'******************************************************************************

Function GetPrinters(objWMIService)

Set colPrnts = objWMIService.ExecQuery("SELECT * FROM Win32_Printer")
Set GetPrinters = colPrnts

End Function

Note that in the script body and in the two functions, each time a new variable or the variable represented by the function names is assigned an object or collection, you have to use Set. Rather than using global variables for the object references, this script passes variables that take on local scope within the two functions.

Of course, this script is an artificial one designed to illustrate the issues involved in passing object references to and returning them from functions. It would be easier to use subs and do all the looping through the collections and displaying the information inside of them. Or better yet, just leave the code in an undivided script.

The script does make a practical point as well, though: if WMI is going to be used in more than one procedure, it's often more efficient to bind to the WMI service and get an object reference to it in the script body rather than binding to the WMI service in each procedure. To make the script yet more practical, we could rename objWMIService g_objWMIService and treat it as a global variable. Rather than passing it as a parameter to the functions, we could use it directly in procedures.

With these modifications, the code binding to the WMI service and the first function call would look like this:

Set g_objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")

Set colProcessors = GetProcessors

The function itself would look like this:

Function GetProcessors

Set colProcs = g_objWMIService.ExecQuery("SELECT * FROM Win32_Processor")
Set GetProcessors = colProcs

End Function

So we'd end up with a couple fewer variables to keep track of and a slightly more streamlined and readable script.

That's Not Real Code

We know, we know. So far Doctor Scripto has broken the implicit Script Shop contract that says: at some point in this column you're going to show us code that actually does something useful for a system administrator. He's tried to show simple scripts that illustrate some points about procedures, but we realize that this is not enough.

So let's talk through that simple function we saw at the beginning of this article that reads a text file into an array. Doctor Scripto has used this function in a lot of scripts in Script Shop because it's a convenient way to get a list of machine names to run the script against (and maintaining a list like this in a text file instead of the script means you don't have to open the script to change it). He's also used it to get lists of processes and services to monitor. He even likes to use it to retrieve his grocery list and send it to the store in e-mail with CDO. (For an example of how you can use this function in different ways in the same script, see "Controlling pest-ware with asynchronous event monitoring.") But he's never sat down and talked his way through it.

strInputFile = "list.txt"

WScript.Echo "Text file contents:"
For Each strItem In ReadTextFile(strInputFile)
  WScript.Echo strItem
Next

'******************************************************************************

Function ReadTextFile(strInputFile)
'Read contents of text file and return array with one element for each line.

On Error Resume Next

Const FOR_READING = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
If Not objFSO.FileExists(strInputFile) Then
  WScript.Echo "Input text file " & strInputFile & " not found."
  WScript.Quit(1)
End If
Set objTextStream = objFSO.OpenTextFile(strInputFile, FOR_READING)
If objTextStream.AtEndOfStream Then
  WScript.Echo "Input text file " & strInputFile & " is empty."
  WScript.Quit(2)
End If
arrLines = Split(objTextStream.ReadAll, vbCrLf)
objTextStream.Close

ReadTextFile = arrLines

End Function

We start by calling the ReadTextFile function and using the value it returns as an implicit array variable within the For Each loop. Then we simply loop through the array and display its contents. At this point in a practical script we could do something more with the array.

The first error check, for whether the file exists, is kind of gilding the lily. If the file doesn't exist, the script can't go on anyway, so it's not so bad to get an error message that the file wasn't found. By handling the error, though, we make the script more communicative and consistent.

If Not objFSO.FileExists(strFilename) Then
  WScript.Echo "Input text file " & strFilename & " not found."
  WScript.Quit(1)
End If

The second error check, for whether the file is empty, is handy because without it the script just runs and returns without doing anything. VBScript and FileSystemObject don't complain, so it's a good idea for the script to display an error message.

If objTextStream.AtEndOfStream Then
  WScript.Echo "Input text file " & strFilename & " is empty."
  WScript.Quit(2)
End If

If the file exists and it's not empty, we use the ReadAll method of the text stream object to slurp all the contents of the file into a string variable, strFileContents. Then we turn the string into an array with the VBScript function Split, dividing it at each line break.

arrLines = Split(objTextStream.ReadAll, vbCrLf)

The function then returns this array of strings in its last line:

ReadTextFile = arrLines

Here, we could have skipped assigning the text stream to a variable and fed the output of Split directly to ReadTextFile as the return value, closing the text stream afterwards.

ReadTextFile = Split(objTextStream.ReadAll, vbCrLf)
objTextStream.Close

But our sense of story-telling overcame the drive for efficiency. Doctor Scripto wanted to dramatize what the script is doing there so he opted to use an extra line to make the script more explanatory.

In most of our scripts, as you may have noticed, the Scripting Guys tend to err on the side of over-explaining. It's not just for our readers: some of us are already receiving solicitations from the American Association of Retired Persons and don't completely trust our memory, so we sometimes need a little help remembering what we were doing when we wrote that code. Maybe Greg was right about the premature senility.

Once it's clear to you how any piece of code works, you should feel free to boil it down as much as you like. That's part of the scripting process.

Deciding When to Use Procedures in Scripts

"So where do I sign up to write procedures?" you're probably saying to yourself by now. Just hold your horses, bucko: Doctor Scripto is not a snake-oil salesman (although he did once sell bridges). Functions and subroutines are not panaceas for all that ails your scripts. Building on his experiences with procedures in this column, he has come up with a few modest prescriptions for when to take a dose of modularity.

When should I consider breaking a script out into procedures?

  • When any code is repeated in more than one place in a script. It usually shortens the script and makes maintenance easier to put this code into a procedure.

  • When code is likely to be reused in other scripts. Common tasks that might be useful in the future can be added to your code library so you and others can avoid having to reinvent the wheel.

  • When breaking a script into procedures clarifies its flow of input, output and internal processing.

  • When inline code gets too lengthy and makes the script hard to read.

  • When a script runs against multiple machines, adding one more layer of complexity to the script.

  • When more than one person is working on the script and work can be conveniently divided among those scripters.

When is it not worth the trouble to use procedures?

  • When the script is simple and short.

  • When procedures would create confusing code in the script body.

  • When you want your script to look more like a program in a system-level language like C++, which must have a main routine and usually includes functions. Scripting saves development effort and cost because scripts have fewer requirements like that. Why fight it?

  • Ask Greg.

Warning: If you experience memory loss or other symptoms, be sure to stop writing procedures immediately. And send mail to Greg.

Where Do I Go to Learn More About Procedure?

A good basic reference is the section on procedures in the VBScript Primer chapter of the Windows 2000 Scripting Guide.

Don't forget, too, that VBScript already contains numerous built-in functions such as Date() and String() that can make life easier. Before you start to write a custom function, check the list in the VBScript documentation on MSDN.

A valuable related resource that can help get your scripts started is the Remote/Multiple Computer Scripting Templates section of the Script Repository. Here you'll find a variety of template code for different kinds of scripts that you can plug procedures into - or not.

Doctor Scripto was going to mention another great source of information, but … umm … it's right on the tip of his tongue.