How to write this script one piece at a time
In the past, I've discussed how to assign network resources based on user and machine identities. However, assigning resources based on user and machine account properties requires that you have an account to inspect. Therefore, I want to show you how to create a script that checks a domain to see whether a user account with a certain name exists and, if it doesn't, how to create an account with that name. Listing 1, page 88, shows this script called CheckNCreateUser.vbs, which takes advantage of Windows Script Host (WSH) and Active Directory Service Interfaces (ADSI). Before you can create a script like CheckNCreateUser.vbs, though, you need to know about procedures.
Procedures are collections of statements that run only when a script calls them. When you call a procedure in a script, the procedure is executed immediately, regardless of where its code appears in the script. Procedures let you easily reuse code in scripts.
VBScript has two types of procedures: Sub procedures (aka subroutines), which you create with the Sub statement, and Function procedures (aka user-defined functions—UDFs), which you create with the Function statement. Subroutines are effectively miniscripts used to execute the same code more than once within a complete script. They don't require arguments and don't necessarily have return values.
UDFs are similar to VBScript's built-in functions in that both types of functions package frequently used sets of instructions that operate on arguments. However, built-in functions aren't Function procedures because Microsoft developers created the built-in functions. Like the name suggests, UDFs are created by VBScript users. Another difference between UDFs and built-in functions is that built-in functions work in any VBScript file, whereas, without special code, UDFs work only in the script in which you've created them. (WSH 5.6 supports a way to reuse code outside a script, but in this article, I look just at UDFs within the body of a script.)
The flow of control in VBScript code is pretty easy to follow: The VBScript runtime engine starts at the top of a script, reads every instruction, then ends when it runs out of instructions or encounters WSH's WScript.Quit method. As a result, if you create a script with errors in line 8 and line 26, the VBScript runtime engine complains only about the error in line 8. It never sees the error in line 26, and it won't until you fix the problem on line 8 and it's able to get to line 26.
You can use conditional statements to slightly change the flow. Conditional statements make the execution of code dependent on the outcome of circumstances for which you're testing (e.g., the existence of a user account before attempting to add the account). However, even with conditional statements, the VBScript runtime engine proceeds methodically through the script from top to bottom. Thus, where you put that Select Case statement or Do...Loop statement matters—unless you put that statement in a procedure.
When you run a script written in VBScript, the VBScript runtime engine examines the script to see whether the code includes any procedures. The runtime engine processes the code in a procedure only if you explicitly invoke that procedure with a Call statement. The Call statement typically includes the Call keyword, followed by the name of the procedure being invoked and any arguments that the procedure needs. (The Call keyword and the arguments are optional.) When the VBScript runtime engine comes across a Call statement, the engine bookmarks that place in the code. The engine then searches the code for a Sub statement or Function statement that includes the same name mentioned in the Call statement. When the runtime engine finds this Sub or Function statement, the engine runs the code that immediately follows, taking into account any conditional statements, until it comes to the End Sub or End Function clause, which marks the end of the procedure. At that point, the runtime engine goes back to its bookmarked location and continues with its methodical path through the script until it comes to another Call statement, runs out of code, or encounters the WScript.Quit method.
If the VBScript runtime engine comes across a Sub statement or Function statement for a procedure that hasn't been called yet, the runtime engine ignores the statement. Therefore, you can define a procedure either before or after you call it. In other words, the location of the procedure's code doesn't matter—nor does the order in which you include multiple procedures. However, a good scriptwriting practice is to put all procedures at the beginning of a script so that you know which procedures the script will call.
You can use procedures for more than just controlling a script's flow. You can also use procedures to perform repetitive actions and store data. For example, having a script perform an action more than once isn't uncommon. One way to have a script repeat an action is to copy the code that carries out the action and paste it wherever necessary.
However, the copy-and-paste approach is prone to error. You can easily miss a line or pick up an extra line when selecting the text to copy. This approach also leads to scripts that are difficult to debug, edit, and maintain. If you need to change the code that carries out the action, you must change each pasted instance. Storing the action in a subroutine or function, then calling it as needed makes the code easier to debug, edit, and maintain.
Procedures can also help you be more flexible about storing data. In previous columns, I've shown you how variables work: You define the variables you plan to use at the beginning of the script (at least, that's how I recommend doing it to simplify debugging), then assign values to those variables. But these variables are, well, variable. Their values can change in the course of a script's execution. Sometimes you want their values to change, but other times you want the variable's value to be specific to a portion of the script and unaffected by anything else that happens in the script. With procedures, you can have "static" variable values. Variables defined and manipulated solely within the context of a procedure are called local variables. The variables available to an entire script are called global variables. Both local and global variables are useful; they're just used for different purposes.
Creating the Script One Piece at a Time
With procedures, you can construct a script by creating smaller, less daunting pieces of code, then assembling those pieces into a script. To construct CheckNCreateUser.vbs, I created a module of code that collects input, a subroutine that makes sure a user account doesn't exist for a specified username, and a subroutine that creates a user account. I then assembled those pieces into a script.
Collecting the Input
Rather than collecting input from the machine on which the script will run, the input module collects input from the administrator launching CheckNCreateUser.vbs. To launch CheckNCreateUser.vbs, the administrator must supply two arguments: the name of the user for which to create the account and the name of the domain in which to create that account. If the administrator doesn't provide this information, the module prompts the person to do so. (Although you could hard-code the username and domain name in the module, you'd have to edit the script each time an administrator wants to search for a different user account, which would be a lot of work and possibly introduce errors.) After the administrator provides the necessary input, the module retrieves the input and stores it in global variables.
Callout C in Listing 1 highlights the input module. The module uses WSH's WScript.Arguments property to retrieve the input as a collection of arguments stored in the WshArguments object. In the WshArguments object, an argument is any combination of characters separated from the next argument by any number of spaces. If you want to use an argument that includes a space, you must enclose it in double quotes. Therefore, Peggy, "french fries", 123456, and //??.. are all valid arguments to store in WshArguments.
WSH numbers the arguments in the order that the administrator inputs them, starting with 0. Therefore, the first argument in the collection is 0, the second argument is 1, and so on. To retrieve a particular argument, you specify WScript.Arguments(x), where x is the number of the argument you want to retrieve. So, for example, if you want to retrieve the fourth argument, you specify WScript.Arguments(3). Even when the collection contains only one argument, you still must specify that you want to retrieve the first argument, WScript.Arguments(0).
You can use the WshArguments object's Count method to count the number of arguments in a collection. In the case of CheckNCreateUser.vbs, the collection must contain two arguments: WScript.Arguments(0), which represents the username, and WScript.Arguments(1), which represents the domain name. If the collection contains fewer than two arguments, the module displays the message Please provide a username and domain name in that order, then quits. If the collection contains two arguments, the module assigns WScript.Arguments(0) to the global variable called sUserName and WScript.Arguments(1) to the global variable called sDomainName.
In the code at callout C, notice that I didn't include any code that makes the script accept arguments. VBScript always accepts arguments; it just won't do anything with them unless you tell it to.
Checking for Existing Accounts
You can't create an account if it already exists, so the QueryForUser subroutine makes sure a user account doesn't exist for the specified username. You can query either a domain or Active Directory (AD) for a user account. Querying the domain works for both Windows NT 4.0 and AD domains, so the QueryForUser subroutine, which callout A shows, queries domains rather than AD.
The QueryForUser subroutine uses ADSI to determine whether a certain user account exists. The subroutine first uses the WinNT namespace to connect to the domain that the sDomainName global variable specifies. The subroutine then uses the IADsContainer interface's Filter property to obtain all the user accounts, which the subroutine puts into an array. The subroutine examines each user account in the array to determine whether the username associated with the account (which the IADs interface's Name property returns) matches the username in the sUserName global variable. VBScript's LCase function lowercases the username in the sDomainName global variable and the username returned by the Name property so that the comparison isn't tripped up by case discrepancies. In most situations, VBScript is case insensitive. However, comparisons made with the = operator are case sensitive because this operator compares the ASCII codes of the involved characters. If a match occurs, the subroutine informs the administrator that the specified user account already exists, then quits.
Because sDomainName's username and the Name property's username are the same case, the script won't overlook a username capitalized differently from the way you supplied it to the script. However, because Windows logons are case sensitive, this script won't work properly if you have two user accounts with the same name but different capitalizations. Christa, christa, and CHRISTA are the same to VBScript but not to Windows.
Although you can use the WinNT namespace for any NT domain type (i.e., NT 4.0 or AD), the namespace is inflexible in that you can search only an entire NT domain and not a particular section of it because SAM domains don't recognize organizational units (OUs). If you have AD, you can use the Lightweight Directory Access Protocol (LDAP) namespace, which lets you refine the search or extend the search.
Creating a User Account
If the specified user account doesn't already exist, the CreateUser subroutine creates the account. This subroutine takes advantage of the administrator's input collected earlier to put the account in the proper domain.
As callout B shows, the CreateUser subroutine again uses the WinNT namespace to connect to the domain that the sDomainName global variable specifies. Next, the subroutine uses the IADsContainer interface's Create method and the username in the sUserName variable to create a user account. The subroutine uses VBScript's Set statement to link, or associate, the new user object with the oUser variable.
Now that an object represents the user account, the subroutine assigns the account a password with the IADsUser interface's SetPassword method. (Although assigning a password in this manner is unrealistic in the real world, this sample code demonstrates how to set the user object's properties.) The subroutine then uses the IADs interface's SetInfo method to write the new user object to the directory. When you work with ADSI, you're not actually manipulating the directory; you're manipulating the cache. You must use the SetInfo method to write to the directory.
The last step in creating the account is to enable the account by setting the account's AccountDisabled property to FALSE. You must use the SetInfo method before you enable the account. If you reverse those steps, you'll get the error message, The specified Active Directory object is not bound to a remote resource.
Assembling the Script
After you've written and tested all the pieces of code, you can assemble those pieces into a script. Relatively speaking, this task is the simplest part of the scriptwriting process, as long as you follow these guidelines:
- Before assembling the script, double-check that each piece works as you expect it to. Troubleshooting a short module or procedure is much easier than troubleshooting an entire script. Knowing that the individual pieces work as expected doesn't eliminate the need to test and debug the assembled script. However, if you encounter a problem with the assembled script, you've narrowed down the possible sources of the problem considerably. If you're having trouble making a piece of code or the entire script work, use the WScript.Echo method to send the output to the screen so that you can see what the code is doing.
- Put all procedures at the beginning of the script. For execution purposes, the procedures' location doesn't matter. As I mentioned previously, procedures won't run until you call them. However, putting them at the top of the script can help remind you what the script should be doing. This placement also makes determining whether a variable should be global or local easier.
- Think about whether to use global or local variables. Local variables make reusing a procedure easier because the associations between the variables and the objects they represent are contained within the procedure. If the value of a global variable changes in the course of a script's execution, a procedure can fail if it uses that global variable and the new value doesn't work. However, associating a variable with an object every time you need the object can slow a script's execution. In CheckNCreateUser.vbs, sUserName and sDomainName must be global variables because their values come from input that the script obtains outside the QueryForUser and CreateUser subroutines. However, oDomain and oUser are local variables in both the QueryForUser and CreateUser subroutines, which makes the subroutines easy to reuse in other scripts.
An Important Step in the Scripting Learning Curve
As CheckNCreateUser.vbs demonstrates, you can use ADSI to not only test for the existence of a user account but also to create an account. This script also demonstrates how to use procedures and how to collect input from administrator-supplied arguments. The latter ability will come in handy as you begin to create administrative scripts that can't gather all the necessary data from a computer or a directory.