Freigeben über


Functions, Subroutines, and How to Call Them From Other Scripts

By The Microsoft Scripting Guys

Sesame Script

Welcome to Sesame Script, the column for beginning script writers. The goal of this column is to teach the very basics of Windows scripting for system administration automation. We’ll provide you with the information you’ll need to begin reading and understanding scripts and to start modifying those scripts to suit your own needs. If there’s anything in particular about scripting you’re finding confusing, let us know; you’re probably not alone.

Check the Sesame Script Archives to see past articles.

On This Page

Functions, Subroutines, and Beyond
What Are Functions and Subroutines?
Use Them Wisely
Sharing Functions and Subroutines
Putting It All Together
The Tricky Part
A Little More Realistic
Parting Words of Wisdom

Functions, Subroutines, and Beyond

This article marks the one-year anniversary of Sesame Script. Yes, it was in June, 2005 that we first published Scripting: Your First Steps. We weren’t even calling it Sesame Script yet, because the Scripting Guy who wrote the article had no intention of writing any more like it. But, due to popular demand (and some other very sneaky Scripting Guys), Sesame Script was born, and here we are at the one year anniversary.

To celebrate, we’re going to tackle one of the great philosophical questions of all time, a question of good versus evil, wrong versus right: “Should I be using functions and subroutines in my scripts?”

Okay, we’re not actually going to tackle that question. This is Sesame Script after all: just how deep do you think we can get here? Instead we’re going to wimp out and take a neutral stance on this issue. We’re just going to give a brief overview of what functions and subroutines are all about, -

Hey, wait a minute! We’re not done yet! We know some of you already know what functions and subroutines are, and you have your own opinions on whether they’re the greatest thing since Microsoft Bob or they’re the worst thing since…Microsoft Bob. If that’s the case, you can skim through the first couple of sections and go directly to the real heart of this article: Sharing Functions and Subroutines between scripts.

That caught your attention, didn’t it? And not just because we italicized it.

Note: If you’d like to read the Scripting Guys’ debate over the controversial topic of functions and subroutines, take a look at the pro-function/subroutine article (which will also take you through a more in-depth explanation of functions and subroutines than we’re going to present here in this article), and the anti-function/subroutine rebuttal article.

What Are Functions and Subroutines?

Functions and subroutines are simply lines of code within a script that have been grouped together and given a name. Here’s an example of a subroutine:

Sub TestSub
    Wscript.Echo "We're in a subroutine."
    Wscript.Echo "This is not a good use of a subroutine."
End Sub

As you can see, this is simply two Wscript.Echo statements grouped together inside Sub and End Sub statements. Following the opening Sub statement you see the name TestSub. This is the name of the subroutine. If we simply pasted this subroutine as-is into Notepad, saved it as a .vbs file and tried to run it, nothing would happen. We wouldn’t get an error message, but we also wouldn’t see the statements we echoed. Why? Because the whole point of naming a subroutine is so that you can use that name to call the subroutine. Try this:

TestSub

Sub TestSub
    Wscript.Echo "We're in a subroutine."
    Wscript.Echo "This is not a good use of a subroutine."
End Sub

As you can see, in our first line we call the TestSub subroutine. That, in turn, runs the two lines of code within the subroutine and echoes the two statements. Now take a look at this subroutine:

Sub TestSub(x)
    Wscript.Echo x
    Wscript.Echo "Still not a good use of a subroutine."
End Sub

This time we’ve included a variable enclosed in parentheses - which is called a parameter or argument - following the name of the subroutine. We do this so we can pass a value from the main part of the script to the subroutine:

x = 4
TestSub x

Sub TestSub(x)
    Wscript.Echo x
    Wscript.Echo "This is not a good use of a subroutine."
End Sub

When you run this script, the value 4 will be echoed to the screen, followed by the string of text. In reality, you could have simply done this:

x = 4
TestSub

Sub TestSub
    Wscript.Echo x
    Wscript.Echo "This is not a good use of a subroutine."
End Sub

In this case we didn’t pass x to the subroutine, but the results will be the same. However, there are several reasons for passing parameters. The whole point of creating a subroutine is to be able to call the same section of code from multiple places in a script. So it stands to reason that when the subroutine is called, the parameter will change, as will the variable passed to the parameter. Kind of like this:

x = 4
y = 3
z = 7

TestSub x

y = x + y + z
TestSub y

Sub TestSub(a)
    Wscript.Echo a
End Sub

In this script, we assign values to several variables. We then call TestSub, passing it the variable x which contains a value of 4. TestSub will then echo that value. After we echo the value we reach the end of the subroutine. At that point we go back to the main part of the script; kind of like we took a side trip and now we have to go back where we turned off and continue straight through our script. So we continue with the line that adds the three variables and assigns that value to the variable y. We then call TestSub again, this time echoing back the value we passed to it. At this point the script ends. Your output will look like this:

4
14

You might have noticed that we passed the variables x and y to the subroutine, but the subroutine used the variable a to echo the value. The names of the variables don’t really matter. We’re not passing the variable to the subroutine, we’re telling the subroutine where the variable is in memory and the subroutine is assigning its own variable to that section of memory. So even though we use the variable a in the subroutine, if we change the value of a in the subroutine that will also change the value of x in the main part of the script, because both variables are looking at the same piece of memory. Give it a try:

x = 7
Wscript.Echo "Before subroutine call x = " & x

TestSub x
Wscript.Echo "After subroutine call x = " & x

Sub TestSub(a)
    Wscript.Echo "In subroutine, original value of a = " & a
    a = 10
    Wscript.Echo "In subroutine, changed value of a = " & a
End Sub

Your output will look like this:

Before subroutine call x = 7
In subroutine, original value of a = 7
In subroutine, changed value of a = 10
After subroutine call x = 10

As you can see, we assigned a value of 7 to the variable x and echoed back that value. We then called TestSub, passing it the variable x. But we’re not passing the value within that variable, we’re actually passing the location in the computer’s memory that the variable is referencing. That location in memory contains the value 7. What that means is that we passed the memory location to TestSub’s parameter, a, so a is now referencing the location in your computer’s memory that’s holding the value 7. In fact, we can see that when we first echo the value of a that value is 7.

We then change the value of a, which changes the value in that memory location. We echo the new value, then exit the subroutine. When we get back to the main part of our script, x is still pointing to that same spot in memory, but now instead of a 7 that spot has a 10 in it, so x has a value of 10.

This is called passing parameters by reference (ByRef), which is the default behavior of VBScript. You can also pass parameters by value. To do this you must specify ByVal in the subroutine:

Sub TestSub(ByVal a)

Passing by value means that the variables are no longer passing references to memory locations, they’re passing actual values. Let’s take a look at the same script, only using the ByVal keyword:

x = 7
Wscript.Echo "Before subroutine call x = " & x

TestSub x
Wscript.Echo "After subroutine call x = " & x

Sub TestSub(ByVal a)
    Wscript.Echo "In subroutine, original value of a = " & a
    a = 10
    Wscript.Echo "In subroutine, changed value of a = " & a
End Sub

This time our output looks like this:

Before subroutine call x = 7
In subroutine, original value of a = 7
In subroutine, changed value of a = 10
After subroutine call x = 7

Taking a look at the output, you can see that changing the value of a within the subroutine did not change the value of x. This is because when we passed x as a parameter, we passed only the value 7, not the location in memory where that 7 is stored. So the variable named a in TestSub received the value 7 and put it in a new location in memory. At this point there are two locations in your computer’s memory that contain the value 7, one location referenced by x and one referenced by a. So when we change the value of a, it has no effect on x.

In other words, if you want to use a value in a subroutine but you don’t want the variable being passed to be changed in any way, then use the ByVal keyword.

Note: Just to add to the confusion, you could have named the variable in TestSub x and the results would be the same: you can change the value in the subroutine, but if you specified ByVal it won’t change the value in the main body of the script because it’s a different variable referencing a different location in memory, even if it does have the same name.

A function works pretty much the same as a subroutine, except that you can use the function in an equation or assignment statement because the function itself returns a value:

x = 4

y = TestFunc(x)
Wscript.Echo y

Function TestFunc(a)
    Wscript.Echo a
    TestFunc = a + 2
End Function

In this script we assign a value to the variable x. We then call TestFunc, passing it the value in x. (Yes you’re right, we’re really passing it the memory location of x because we didn’t specify ByVal.) Notice how we put the parameter in parentheses when we called the function. This is required when you put a function call within some sort of equation, as we did here. We’re assigning the value returned by the function to the variable y. And how did the function get a value? Like this:

TestFunc = a + 2

In the last line of the function we assign a value to the name of the function. That’s all there is to it. Here’s the output:

4
6

Use Them Wisely

Throughout the United States you’ll see signs along many highways that say “Keep right except to pass.” Many people ignore these signs and stay to the left regardless because, well, the left lane is there, so they want to use it. The same is true of functions and subroutines. People use them because they kind of like the whole idea, they seem to be convenient, and, well, they want to use them. But just like on the road, this can really annoy the people who have to follow you. If you don’t really need a function or subroutine, don’t use it. And keep right except to pass.

Note: We know this issue causes great distress to many people. We know that, we’ve acknowledged it, so you can stop your angry email mid-sentence, take a deep breath, and let it go.

When would you use a function or a subroutine? Typically they’re used in long scripts, much longer than you’ll ever see in Sesame Script. Often they’re used for error handling, when you want to perform a single set of actions based on errors that could happen in many places in your script. To see some examples, read just about any article in the Doctor Scripto’s Script Shop archive.

Sharing Functions and Subroutines

For those of you who skimmed over the overview of functions and subroutines (or skipped it altogether), this is the part of the article you’ve been waiting for. We’re going to talk about a scripting technique that can be useful, but also a little tricky. For those of you who’ve worked with compiled programming languages such as C#, Visual Basic, C++, and many others, this will seem like an old friend (although still a tricky friend). To everyone else, this is kind of a new and interesting thing that can make your life easier, but can also create some confusion. What we’re going to do is put functions and subroutines in one script, and call those functions and subroutines from another script. Pretty radical, huh?

Note: Why is this radical, you ask? It has to do with the way VBScript works versus the way compiled languages work. In compiled languages you can put something into a program called an include statement. Include statements allow you to pull objects from other programs, typically libraries such as DLLs, into your program. These libraries contain things such as classes, methods, functions, subroutines, and so on that you can then use in your program. Why is this such a radical idea then? Because VBScript doesn’t allow you to do this. VBScript scripts are not compiled, so they can’t pull in elements from other places. What we’re going to show you here is a way to work around that, and make it seem like VBScript uses an include statement.

The way we pull functions and subroutines from one file into another is by using the Execute statement:

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("procedures.vbs", ForReading)
Execute objFile.ReadAll()

We start by creating a constant, which we’ll use to open the file for reading (rather than for writing). We create an instance of the FileSystemObject, then call the OpenTextFile method on that object to open the file that contains our functions and subroutines. We read the contents of the file using the ReadAll method, then call the Execute statement on those contents. Calling Execute on the results of ReadAll basically stores the contents of the file in memory where you can use them while your script is running. That means that, from this point on in your script, you can call any of the functions and subroutines within Procedures.vbs.

Note: We’ve named our procedures file Procedures.vbs. We used a .vbs extension just to signify that the file contains VBScript code. However, because this file contains only a function it can’t be run by itself (if you double-click the file or run it using Cscript nothing will happen). This file could just as easily be named Procedures.txt, VBSLibrary.txt, or whatever else you choose.

Time to look at an example? Yes, we think so too. Let’s say we want to read the first line from every text file in a folder. Reading from a text file is a pretty common occurrence, so we’ve created a function that does just that: it opens a file and reads the first line. But before we can do that, we have to find all the text files in the folder. Here’s how we do that:

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

Set colFiles = objWMIService.ExecQuery _
    ("ASSOCIATORS OF {Win32_Directory.Name='C:\Scripts'} Where " _
        & "ResultClass = CIM_DataFile")

For Each objFile in colFiles
    If objFile.Extension = "txt" Then
        Wscript.Echo objFile.Name

        strText = strText & ReadFile(objFile) & vbCrLf

    End If
Next
Wscript.Echo strText

We start by connecting to the WMI service on the local computer (although you could connect to a different computer by changing the value of strComputer from “.” to the name of the computer). We then query the WMI class Win32_Directory for all the files within the C:\Scripts folder:

Set colFiles = objWMIService.ExecQuery _
    ("ASSOCIATORS OF {Win32_Directory.Name='C:\Scripts'} Where " _
        & "ResultClass = CIM_DataFile")

We now need to go through those files and determine which ones are text files. We do that by setting up a For Each loop then checking to see if the file extension is equal to txt:

For Each objFile in colFiles
    If objFile.Extension = "txt" Then

If the file is a text file, we echo the name of the file, then call our function, ReadFile, to read the text file and return the first line from that file:

strText = strText & ReadFile(objFile) & vbCrLf

Notice that we pass an object reference to the file to our ReadFile method. (If you’re not sure what an object reference is, take a look at the article Class is in Session.)

At the end of the script we echo back strText, which should display the first line of every text file in the C:\Scripts folder.

So what does our Procedures.vbs file look like? Just like this:

Function ReadFile(oFile)
    Const ForReading = 1

    Set objFSO = CreateObject("Scripting.FileSystemObject")
    Set objTextFile = objFSO.OpenTextFile _
        (oFile.Name, ForReading)

    ReadFile = objTextFile.ReadLine
    objTextFile.Close
End Function

This function accepts an object as its parameter. As we saw, this object is the text file we want to read. The purpose of the function is to open that file, read the first line, and return the line to the calling script.

We start by once again defining the constant ForReading. Yes, we already did that in our main script when we opened this script file. But although our main script can read everything in Procedures.vbs, nothing in Procedures.vbs can read from the main script. So we need to define the constant that we’ll be using to open the text file for reading.

We then create an instance of the FileSystemObject and use that object to open the text file:

Set objTextFile = objFSO.OpenTextFile _
        (oFile.Name, ForReading)

Notice that we used the Name property of the file that was passed in (oFile.Name) to open the file. The Name property returns the full path to the file, including filename.

Now that we have an object reference to the text file, we can call the ReadLine method to read the first line in the file:

ReadFile = objTextFile.ReadLine

We assign the value returned from ReadLine to ReadFile, which just happens to be the name of our function. This is the value that will be returned to our main script when we call the ReadFile function.

After that we close the file. At this point the function ends and the main script will continue.

Putting It All Together

In case you missed it, here’s the whole thing all put together:

Main.vbs

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("procedures.vbs", ForReading)
Execute objFile.ReadAll()

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

Set colFiles = objWMIService.ExecQuery _
    ("ASSOCIATORS OF {Win32_Directory.Name='C:\Scripts'} Where " _
        & "ResultClass = CIM_DataFile")

For Each objFile in colFiles
    If objFile.Extension = "txt" Then
        Wscript.Echo objFile.Name
        
        strText = strText & ReadFile(objFile) & vbCrLf


    End If
Next
Wscript.Echo
Wscript.Echo strText

Procedures.vbs

Function ReadFile(oFile)
    Const ForReading = 1

    Set objFSO = CreateObject("Scripting.FileSystemObject")
    Set objTextFile = objFSO.OpenTextFile _
        (oFile.Name, ForReading)

    ReadFile = objTextFile.ReadLine
    objTextFile.Close
End Function

We start Main.vbs by opening Procedures.vbs and calling Execute on the ReadAll method for Procedures.vbs. We can now call the ReadFile function, defined in Procedures.vbs, from our script in Main.vbs.

We’ve kept our example fairly simple for the sake of, um, simplicity. But in reality your procedures file could contain as many functions and subroutines as you want, so, as we mentioned, it can work like a library, with all your most-commonly used tasks in one place. And that’s the really useful and cool part of this whole process.

The Tricky Part

We mentioned there was a tricky part to this, didn’t we? Take a look at the For Each loop of our main script:

For Each objFile in colFiles
    If objFile.Extension = "txt" Then
        Wscript.Echo objFile.Name

        strText = strText & ReadFile(objFile) & vbCrLf

    End If
Next

If someone opens this script to see how it works, or to update it, or to use it as a template for another script, or any of dozens of other reasons, this could cause some confusion. As you read through this script, it seems pretty straight-forward: read through the files, find the files with a “txt” extension, echo the filename, call the ReadFile method….

But wait a minute, where’s the ReadFile method? What does it do? Yes, you can look at the beginning of the script and see that we read and executed a .vbs file and that should give you a hint, but now you have to go look through that file. This could be a minor inconvenience, but here are a couple other things to watch out for:

  • If you have some functions in your main script, and some in your Procedures.vbs (or whatever you named it) file, you need to go looking around to figure out where each function is. This isn’t all that tricky or confusing in a short script, but if you have a long script with a lot of function or subroutine calls, and maybe you’re using functions and subroutines from more than one procedures file, it can get very difficult to track everything down and figure out how it all works. And debugging can start to turn into a nightmare.

  • You need to be very careful what you name your functions and subroutines. For example, suppose you create a function in your script named DoSomething. You also read in a procedures file that contains a function named DoNothing that you want to use. You read in the procedures file so you can call the DoNothing function, but what you didn’t realize was that the procedures file had another function in it named DoSomething. Now you have two DoSomething functions, one in the procedures file and one in your script. Another debugging difficulty.

  • If you copy your script to another machine or give it to someone else, you must remember to copy or give the procedures file along with it. The script will not run without the procedures file. (And because we didn’t specify a full path, the procedures file must be in the same folder as the script file. If we had specified a full path, the procedures file must be placed in that same location on every machine on which the script is run.)

A Little More Realistic

We’ve walked you through a very simple example just to show you how this functionality works. Here’s another example that we’re not going to go through in any detail, but we wanted to show you a situation where using a procedures file can be helpful.

Procedures.vbs

This procedures file has one function and one subroutine. The GetWMICollection function connects to the WMI service on the specified computer and retrieves the collection of objects from the specified WMI class. It takes as parameters the computer name and the name of the class, and returns the collection of objects. The WriteOutput subroutine opens a file (and creates it if it doesn’t exist) and appends text to that file. It takes as parameters the name of the file and the text to be appended.

Notice also that we’ve declared a constant outside of the function and the subroutine. Constants work the same way as the functions and subroutines; this constant (ForAppending) can be used in the calling script.

Const ForAppending = 8

Function GetWMICollection(strComputer, strClass)
    Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")

    Set GetWMICollection = objWMIService.ExecQuery _
        ("Select * from " & strClass)
End Function

Sub WriteOutput(strFileName, strText)
    Set objFSO = CreateObject("Scripting.FileSystemObject")
    Set objTextFile = objFSO.OpenTextFile _
        (strFileName, ForAppending, True)
    objTextFile.WriteLine strText
    objTextFile.Close
End Sub

Processes.vbs

This script retrieves a list of processes running on a computer and writes that list to a file. It calls the GetWMICollection function from our procedures file to retrieve the list of processes, passing it the name of the computer and the class name (Win32_Process). The script then loops through the list of returned processes and calls WriteOutput to write each process to the output file specified in the first parameter. (The second parameter contains the name of the process.)

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("procedures.vbs", ForReading)
Execute objFile.ReadAll()

strComputer = "."
strOutFile = "C:\Scripts\Processes.txt"

Set colProcessList = GetWMICollection(strComputer, "Win32_Process")

For Each objProcess in colProcessList
    WriteOutput strOutFile, objProcess.Name
Next

Services.vbs

This script is similar to the Processes.vbs script we just looked at, but instead of returning a list of processes it returns a list of services. This script uses the same function and subroutine as the previous script to retrieve the list of services and write them to a text file.

Const ForReading = 1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("procedures.vbs", ForReading)
Execute objFile.ReadAll()

strComputer = "."
strOutFile = "C:\scripts\services.txt"

Set colListofServices = GetWMICollection(strComputer, "Win32_Service")

For Each objService in colListOfServices
    WriteOutput strOutFile, objService.DisplayName
Next

Parting Words of Wisdom

So here’s some closing thoughts on this whole thing: use this functionality carefully. It can be a neat way to gather all your functions and subroutines together and a great way to reuse script code, but if you’re not careful it can cause some pretty big debugging and maintenance headaches down the road. Where you should be in the right lane, unless you’re passing.